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.
This commit is contained in:
2026-08-12 14:48:17 +00:00
parent 4de67e4bea
commit 2685e9ad06
2 changed files with 11 additions and 5 deletions
+4 -1
View File
@@ -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"
+7 -4
View File
@@ -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)