From 4de67e4bea4023def090e2ac41bbc4ae068e713b Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 12 Aug 2026 14:47:36 +0000 Subject: [PATCH] docs: Separate caller mistakes from backend failures in the plan createToken's catch-all answered 400 for every unmatched error, so a database failure reported itself as the caller's malformed request. Found in review of Task 7. --- .../plans/2026-08-12-api-tokens-openapi.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-12-api-tokens-openapi.md b/docs/superpowers/plans/2026-08-12-api-tokens-openapi.md index 507a7e5..abb69e0 100644 --- a/docs/superpowers/plans/2026-08-12-api-tokens-openapi.md +++ b/docs/superpowers/plans/2026-08-12-api-tokens-openapi.md @@ -495,6 +495,11 @@ 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. It is what lets the handler answer 400 for a bad request and + // 500 for a database that is down, rather than blaming the caller for both. + ErrTokenInvalid = errors.New("invalid token request") ) // TokenPrefix is on every plaintext so a leaked value is recognisable in a log @@ -1313,9 +1318,14 @@ 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: + // Anything left is a backend failure. Reporting it as 400 tells the + // caller their request was malformed while the database is down. + c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create token"}) + return } expiry := "no expiry"