Skip to content

feat(callback): centralize logging and add errorRedirectUrl option#74

Merged
nicknisi merged 3 commits intomainfrom
feat/callback-error-handling
May 1, 2026
Merged

feat(callback): centralize logging and add errorRedirectUrl option#74
nicknisi merged 3 commits intomainfrom
feat/callback-error-handling

Conversation

@nicknisi
Copy link
Copy Markdown
Member

@nicknisi nicknisi commented Apr 29, 2026

Summary

  • Centralizes callback error logging. Every error path through handleCallbackRoute now emits exactly one [authkit-tanstack-react-start] OAuth callback failed: line via a single log point at the top of errorResponse. Removes the two scattered console.error calls at the previous setup-failure and handle-callback-failure sites. The missing-code and onSuccess-throws paths now also log uniformly. buildVerifierDeleteHeaders's own auxiliary log is left untouched (different concern).
  • Adds opt-in browser-friendly redirect via errorRedirectUrl. New string option on HandleCallbackOptions. When set (and onError is not), errorResponse returns a 302 with Location set to the resolved URL plus the verifier-delete cookies. Accepts absolute URLs and relative paths (resolved against request.url). Malformed values fall back to the existing path-dependent JSON response (400 or 500) with a separate config-warning log line.
  • Preserves existing semantics. onError still wins precedence when both are set. Errors thrown inside onError are explicitly NOT caught by the SDK — locked in by a regression test against the new errorRedirectUrl try/catch broadening. JSON-fallback shape, status codes, and delete-cookie headers unchanged when neither option is set.

The customer incident this addresses: a stale verifier cookie or PKCE state mismatch caused the SDK to surface raw JSON to the user's browser. Callers who set errorRedirectUrl now bounce users to a route they own (e.g., /sign-in?error=auth_failed) with verifier-delete cookies still attached. Callers who want observability can wire Sentry inside onError (or rely on the central console.error ingestion) and get a uniform signal.

What's in the diff

File Change
src/server/types.ts Adds errorRedirectUrl?: string field with JSDoc covering relative/absolute resolution, precedence vs onError, malformed-value fallback, and open-redirect warning. Adds throw-not-caught note to onError JSDoc.
src/server/server.ts Removes console.error at the prior setup-failure and handle-callback-failure call sites. Adds central console.error at the top of errorResponse. Adds errorRedirectUrl branch between the onError short-circuit and JSON fallback, with a tightly scoped try/catch around new URL() that logs a config warning and falls through to JSON on malformed input.
src/server/server.spec.ts Promotes console.error spy to top-level beforeEach/afterEach. Adds 12 new test cases (4 from it.each for log-fires-once across all error paths, plus explicit cases for absolute/relative errorRedirectUrl, precedence, malformed-URL fallback on both 400 and 500 paths, onSuccess-throws routing, returnPathname-ignored-on-error, and a regression test that onError throws are not caught).
README.md Replaces handleCallbackRoute section with three examples (basic, sign-in error page, Sentry capture), updates the Options list with the new field, and adds the precedence + open-redirect notes.

Backward compatibility

Non-breaking. With neither onError nor errorRedirectUrl set, the response is semantically identical to today: 400 for missing code, 500 for setup/handleCallback/onSuccess failures, Content-Type: application/json, same JSON body shape, same delete-cookie headers. Existing tests (and the example app's existing onError-using callback) continue to pass.

Test plan

  • pnpm test src/server/server.spec.ts — 32/32 (was 20/20 before).
  • pnpm test — full suite, 215/215.
  • pnpm build — typecheck + library build.
  • cd example && pnpm build — no Node-only externalization warnings in the client chunk.
  • Manual end-to-end in the example app:
    • Default (no options): missing-code shows JSON 400, exactly one OAuth callback failed: log line.
    • errorRedirectUrl only: 302 Location + verifier cookies cleared.
    • onError + errorRedirectUrl: onError wins, both logs fire (central + user's), no redirect.
    • Malformed errorRedirectUrl (http://[::1): falls back to JSON, both logs fire in order (central first, malformed second).
    • Production reproducer (stale verifier cookie + real OAuth flow with errorRedirectUrl: '/sign-in?error=auth_failed'): browser lands on /sign-in?error=auth_failed instead of raw JSON.

Out of scope

Per the contract:

  • Re-throw-by-default behavior — would be breaking; defer until coordinated with authkit-nextjs.
  • Sentry SDK as a dependency — caller wires it inside their own onError.
  • URL-scheme allowlist for errorRedirectUrl — developer-controlled value; documented as such.
  • Aligning authkit-nextjs to match — separate repo, separate release.

Open in Devin Review

Centralizes the per-error-path `console.error` calls in `errorResponse` so every
callback failure (missing code, getAuthkit throws, handleCallback throws,
onSuccess throws) emits exactly one `[authkit-tanstack-react-start] OAuth
callback failed:` line. Removes the two scattered logs at the prior call sites.

Adds a new opt-in `errorRedirectUrl` option to `HandleCallbackOptions`. When
set (and `onError` is not), `errorResponse` returns a 302 to the resolved URL
with the verifier-delete cookies attached. Accepts absolute URLs and relative
paths (resolved against the request origin). Malformed values fall back to the
existing path-dependent JSON response with a separate config-warning log.

`onError` still wins precedence when both are set, and errors thrown inside
`onError` are explicitly NOT caught (locked in by a regression test).

Updates types JSDoc, README (Sentry + sign-in error page examples + open-
redirect note), and adds 12 new test cases covering all error paths.
devin-ai-integration[bot]

This comment was marked as resolved.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 29, 2026

Greptile Summary

This PR centralises error logging in handleCallbackRoute to a single console.error call at the top of errorResponse, and adds an opt-in errorRedirectUrl option that returns a 302 redirect (with verifier-delete cookies) instead of raw JSON when a callback fails. Both previously-raised issues (swallowed setupError and hardcoded "500" in the fallback log message) appear to have been addressed in the current revision.

Confidence Score: 5/5

Safe to merge — no new P0/P1 issues found; both previously raised concerns are addressed in the current revision.

All error paths are covered by tests (32/32), the two prior review findings (swallowed setupError and the dynamic status in the fallback log) are fixed in the current code, the open-redirect risk is explicitly documented and scoped to developer-controlled config, and backward compatibility is preserved.

No files require special attention.

Important Files Changed

Filename Overview
src/server/server.ts Centralises logging to a single console.error at the top of errorResponse; adds errorRedirectUrl branch with try/catch URL parsing and graceful fallback; passes setupError (instead of a synthetic error) to errorResponse when getAuthkit() rejects.
src/server/server.spec.ts Promotes console.error spy to top-level beforeEach/afterEach; adds 12 new test cases covering errorRedirectUrl (absolute, relative, malformed on 400/500 paths), onError precedence, onSuccess-throws routing, returnPathname ignored on error, and regression for onError throws not being caught.
src/server/types.ts Adds errorRedirectUrl?: string field with thorough JSDoc covering relative/absolute resolution, onError precedence, malformed-value fallback, open-redirect warning, and no-catch behaviour for onError throws.
README.md Replaces the handleCallbackRoute section with three updated examples (basic, errorRedirectUrl, Sentry); updates the Options list with the new field including precedence and open-redirect notes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[GET /api/auth/callback] --> B{getAuthkit}
    B -- success --> C{code present?}
    B -- throws --> D[setupError captured]
    D --> C
    C -- no --> E[errorResponse\n400]
    C -- yes --> F{authkit initialized?}
    F -- no --> G[errorResponse\n500, passes setupError]
    F -- yes --> H[authkit.handleCallback]
    H -- throws --> I[errorResponse\n500]
    H -- success --> J[options.onSuccess?]
    J -- throws --> I
    J -- success --> K[307 redirect]

    E --> L[console.error central log]
    G --> L
    I --> L

    L --> M{options.onError?}
    M -- yes --> N[call onError\nappend delete-cookies\nthrows propagate up]
    M -- no --> O{options.errorRedirectUrl?}
    O -- yes --> P{URL valid?}
    P -- yes --> Q[302 redirect\n+ delete-cookies]
    P -- no / malformed --> R[log config warning\nfall through]
    R --> S[JSON fallback\n400 or 500\n+ delete-cookies]
    O -- no --> S
Loading

Reviews (3): Last reviewed commit: "Fix swallowed setup error and hardcoded ..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

nicknisi and others added 2 commits April 30, 2026 09:06
Preserve the original getAuthkit() error instead of replacing it with a
synthetic 'AuthKit not initialized' message so the central log emits
the real failure reason.

Interpolate defaultStatus into the malformed errorRedirectUrl log
message so it correctly says 'JSON 400' on the missing-code path
instead of always saying 'JSON 500'.

Co-Authored-By: nick.nisi@workos.com <nick@nisi.org>
@nicknisi nicknisi requested a review from gjtorikian April 30, 2026 14:21
@nicknisi nicknisi merged commit 2ce6189 into main May 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants