Skip to content

fix(oauth): route 400 token-exchange errors (invalid_grant) to onError#526

Open
mendrinos wants to merge 1 commit into
atinux:mainfrom
mendrinos:fix/token-exchange-400-onerror
Open

fix(oauth): route 400 token-exchange errors (invalid_grant) to onError#526
mendrinos wants to merge 1 commit into
atinux:mainfrom
mendrinos:fix/token-exchange-400-onerror

Conversation

@mendrinos
Copy link
Copy Markdown
Contributor

@mendrinos mendrinos commented May 31, 2026

🔗 Linked issue

Fixes #525

❓ Type of change

  • 🐛 Bug fix (a non-breaking change that fixes an issue)

📚 Description

requestAccessToken only intercepted 401 token-exchange failures and routed them to onError; every other status was re-thrown. But per RFC 6749 §5.2 the token endpoint returns 400 Bad Request for the common OAuth failures (invalid_grant, invalid_request, unauthorized_client, unsupported_grant_type, invalid_scope) — only invalid_client may use 401.

As a result, the most common real-world failure — 400 invalid_grant from a reused/expired one-time authorization code (back/forward navigation, callback refresh, link prefetch/scanner, prompt=none silent re-auth) — bypassed onError and surfaced as an unhandled error. Reproduced on Google, Facebook and Microsoft, which all share requestAccessToken.

This PR routes any 4xx response that carries a structured OAuth error body to the existing handleAccessTokenErrorResponse/onError path, while still re-throwing genuine network errors, 5xx, and opaque non-OAuth responses.

-    /**
-     * For a better error handling, only unauthorized errors are intercepted, and other errors are re-thrown.
-     */
-    if (error instanceof FetchError && error.status === 401) {
-      return error.data
-    }
-    throw error
+    /**
+     * Per RFC 6749 §5.2 the token endpoint returns a 4xx status (usually 400,
+     * e.g. `invalid_grant`) with a structured `{ error }` body for OAuth
+     * failures — only `invalid_client` may use 401. Intercept any such
+     * structured error response so it is routed to `onError`, and re-throw
+     * everything else (network errors, 5xx, opaque non-OAuth responses).
+     */
+    if (
+      error instanceof FetchError
+      && error.status && error.status >= 400 && error.status < 500
+      && error.data?.error
+    ) {
+      return error.data
+    }
+    throw error

Why gated on error.data?.error: ensures only structured OAuth error responses (per §5.2) are swallowed — opaque 4xx HTML/proxy errors still throw. Backward compatible: 401 invalid_client continues to be routed to onError exactly as before.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

  • Bug Fixes
    • Improved authentication error handling to better capture and report OAuth token endpoint failures, enabling more comprehensive error detection and reporting.

Per RFC 6749 §5.2 the token endpoint returns 4xx (usually 400, e.g.
invalid_grant) with a structured { error } body for OAuth failures; only
invalid_client may use 401. requestAccessToken previously intercepted
only 401 and re-threw everything else, so 400 invalid_grant bypassed
onError and surfaced as an unhandled error. Route any 4xx response
carrying a structured error body to onError; re-throw network/5xx/opaque
errors.

Fixes atinux#525

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b57564f5-c125-4e1a-a26a-810a45a07739

📥 Commits

Reviewing files that changed from the base of the PR and between ceb366b and 180c4e5.

📒 Files selected for processing (1)
  • src/runtime/server/lib/utils.ts

📝 Walkthrough

Walkthrough

The requestAccessToken function now catches 4xx OAuth token endpoint failures with structured error bodies per RFC 6749, instead of only intercepting 401 responses. This enables proper error routing for the more common 400 responses (e.g., invalid_grant).

Changes

OAuth Error Handling

Layer / File(s) Summary
OAuth 4xx Error Handling
src/runtime/server/lib/utils.ts
requestAccessToken now catches FetchError responses with 4xx status codes and a structured error field in error.data, returning the OAuth error data instead of only handling 401 responses. Other errors are re-thrown.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A rabbit hops through error paths,

Where tokens fail and codes clash—

Four-hundred's dance, now understood,

RFC guides us as it should! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly describes the main change: fixing OAuth 400 errors to route to onError via the invalid_grant error case.
Linked Issues check ✅ Passed The code change addresses all primary requirements from issue #525: intercepts 4xx OAuth errors with structured error bodies and routes them to the existing handler instead of re-throwing.
Out of Scope Changes check ✅ Passed The changes are narrowly scoped to the catch block in requestAccessToken and directly address the issue without introducing unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

Token-exchange 400 (invalid_grant) bypasses onError and becomes unhandled — only 401 is intercepted

1 participant