Skip to content

feat(api): Auth errors carry a machine-readable code (#357) - #372

Merged
bitbiter-dev merged 3 commits into
masterfrom
feat/357-auth-error-codes
Oct 29, 2026
Merged

bitbiter-dev merged 3 commits into
masterfrom
feat/357-auth-error-codes

Conversation

@bitbiter-dev

Copy link
Copy Markdown
Owner

Closes #357

Summary

Auth failures change from an English sentence to Problem Details with a stable code. Status codes are unchanged.

 POST /api/v1/auth/login   → 401
-{ "error": "This account has been disabled. Please contact an administrator." }
+application/problem+json
+{ "status": 401, "code": "account_disabled", "detail": "This account has been disabled. …" }

 POST /api/v1/auth/register → 409
-{ "error": "Username is already taken." }
+{ "status": 409, "code": "username_taken", "detail": "…", "errors": { "username": ["username_taken"] } }
Endpoint Codes
login, login/mobile invalid_credentials 401, account_disabled 401, account_locked 429 + Retry-After
refresh refresh_token_required 401, refresh_token_invalid 401, account_disabled 401, account_locked 429 + Retry-After
register field: invalid_username/username_taken → username, invalid_email/email_taken → email, password_too_short/_too_long/_pwned → password; plain: invite_token_invalid 422
change-password field: invalid_credentials 400 → currentPassword, password_* 422 → newPassword
rate limiter rate_limited 429 + Retry-After

The rate limiter now sends Retry-After on every rejection. Both policies are sliding windows, which report no retry time, so this header was never sent before. Each policy now sends its whole window as Retry-After:

 SlidingWindowPolicy.OnRejected
-  if lease has RetryAfter (never, for a sliding window)
-    set Retry-After
+  set Retry-After = window   (60 s sensitive, 900 s refresh)
   write rate_limited problem

The following were decided with the maintainer while implementing:

Admin endpoints are out of scope.

Evidence

The new tests (AuthErrorContractTests) post to the real endpoints on the in-process host. Each one asserts the status, the content type, code, detail, errors and Retry-After against literal values.

  • Before: all 26 cases fail on the content type (application/json, not application/problem+json). The rate-limit cases fail on a missing Retry-After.
  • After: the API unit suite passes, 671 of 671. scripts/mutation-changed.sh on the five changed source files reports 100.00%.

Merge Danger

Door: two-way

Nothing is deployed, and no client consumes these bodies yet. #360 is the first.

Blast Radius: auth-responses

Only the error bodies of the auth endpoints change, plus Retry-After on 429. Success responses and status codes are untouched.

🤖 Generated with Claude Code

bitbiter-dev and others added 3 commits October 29, 2026 22:15
Every failure of login (bearer and web), register, refresh and
change-password, and every rate-limiter rejection, is now
application/problem+json with a stable `code` extension and the old
English text in `detail`. Failures of one request field are
ValidationProblems with the code listed under that field in `errors`
(username, email, password, currentPassword, newPassword). Status
codes are unchanged.

The sliding-window limiters report no retry time, so a rate-limited
response never carried Retry-After. Each policy now answers with its
whole window (60 s sensitive, 900 s refresh), after which every permit
is back.

Contract tests run the real endpoints, mapper and limiter on the
in-process test host and pin each code, status, content type and
Retry-After with literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The password failures' mapping throws for an error it does not own
instead of returning null for the caller to throw. The contract tests
assert every problem through one helper, so each checks its status in
the body too, and wrong credentials and lockout run on both login
endpoints. The change-password endpoint test stubs a problem rather
than the removed { error } shape. Doc comments say what, not why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bitbiter-dev
bitbiter-dev merged commit ebbabf9 into master Oct 29, 2026
17 checks passed
@bitbiter-dev
bitbiter-dev deleted the feat/357-auth-error-codes branch October 29, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth errors carry a machine-readable code

1 participant