From 2685e9ad0658d0388573c5335869b17fabace1f3 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 12 Aug 2026 14:48:17 +0000 Subject: [PATCH] fix: Distinguish caller mistakes from backend failures in CreateAPIToken createToken's catch-all mapped every unmatched error to 400, so a database outage reported itself as a malformed client request. Wrap the genuine validation failures with ErrTokenInvalid and let the handler answer 500 with a fixed message for everything else. --- server/internal/api/tokens.go | 5 ++++- server/internal/services/tokens.go | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/internal/api/tokens.go b/server/internal/api/tokens.go index 4d76341..c7c0711 100644 --- a/server/internal/api/tokens.go +++ b/server/internal/api/tokens.go @@ -62,9 +62,12 @@ func createToken(c *gin.Context) { case errors.Is(err, services.ErrInvalidScope): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "code": "invalid_scope"}) return - case err != nil: + case errors.Is(err, services.ErrTokenInvalid): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return + case err != nil: + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create token"}) + return } expiry := "no expiry" diff --git a/server/internal/services/tokens.go b/server/internal/services/tokens.go index fcf76f4..86fc27f 100644 --- a/server/internal/services/tokens.go +++ b/server/internal/services/tokens.go @@ -20,6 +20,9 @@ var ( ErrTokenNameTaken = errors.New("a token with that name already exists") ErrTokenRoleTooHigh = errors.New("cannot create a token above your own role") ErrTokenExpiryPolicy = errors.New("expiry exceeds this instance's maximum token lifetime") + // ErrTokenInvalid marks a caller mistake as distinct from a backend + // failure, which is what lets the handler choose 400 or 500. + ErrTokenInvalid = errors.New("invalid token request") ) // TokenPrefix is on every plaintext so a leaked value is recognisable in a log @@ -58,10 +61,10 @@ func LowerRole(a, b string) string { func CreateAPIToken(instanceID, userID, name, role string, scopes []string, expiresInDays *int, ip string) (*models.APIToken, string, error) { name = strings.TrimSpace(name) if name == "" || len(name) > tokenNameMax { - return nil, "", fmt.Errorf("token name must be 1 to %d characters", tokenNameMax) + return nil, "", fmt.Errorf("%w: token name must be 1 to %d characters", ErrTokenInvalid, tokenNameMax) } if !models.ValidRole(role) { - return nil, "", fmt.Errorf("invalid role %q", role) + return nil, "", fmt.Errorf("%w: invalid role %q", ErrTokenInvalid, role) } if err := ValidScopes(scopes); err != nil { return nil, "", err @@ -69,7 +72,7 @@ func CreateAPIToken(instanceID, userID, name, role string, scopes []string, expi owner, err := GetUserInInstance(instanceID, userID) if err != nil { - return nil, "", fmt.Errorf("user not found") + return nil, "", fmt.Errorf("%w: user not found", ErrTokenInvalid) } if roleRank(role) > roleRank(owner.Role) { return nil, "", ErrTokenRoleTooHigh @@ -85,7 +88,7 @@ func CreateAPIToken(instanceID, userID, name, role string, scopes []string, expi switch { case expiresInDays != nil: if *expiresInDays <= 0 { - return nil, "", fmt.Errorf("expires_in_days must be positive") + return nil, "", fmt.Errorf("%w: expires_in_days must be positive", ErrTokenInvalid) } if maxDays > 0 && *expiresInDays > maxDays { return nil, "", fmt.Errorf("%w: maximum is %d day(s)", ErrTokenExpiryPolicy, maxDays)