From bf9ead5fe598731cf2119ee3cfc0e8e6e19afe1a Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Wed, 29 Apr 2026 14:56:18 -0500 Subject: [PATCH 1/3] feat(callback): centralize logging and add errorRedirectUrl option 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. --- README.md | 39 +++++--- src/server/server.spec.ts | 187 +++++++++++++++++++++++++++++++++++--- src/server/server.ts | 22 ++++- src/server/types.ts | 30 ++++++ 4 files changed, 251 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 0055eac..32f8415 100644 --- a/README.md +++ b/README.md @@ -411,26 +411,38 @@ export const Route = createFileRoute('/api/auth/callback')({ }); ``` -**With hooks for custom logic:** +**With a sign-in error page (browser-friendly default):** ```typescript +export const Route = createFileRoute('/api/auth/callback')({ + server: { + handlers: { + GET: handleCallbackRoute({ + errorRedirectUrl: '/sign-in?error=auth_failed', + }), + }, + }, +}); +``` + +The user lands on `/sign-in?error=auth_failed` (a route you own) instead of seeing raw JSON. Verifier-delete cookies are still attached. + +**With Sentry capture:** + +```typescript +import * as Sentry from '@sentry/node'; + export const Route = createFileRoute('/api/auth/callback')({ server: { handlers: { GET: handleCallbackRoute({ onSuccess: async ({ user, authenticationMethod }) => { - // Create user record in your database await db.users.upsert({ id: user.id, email: user.email }); - // Track analytics analytics.track('User Signed In', { method: authenticationMethod }); }, onError: ({ error, request }) => { - // Custom error handling - console.error('Auth failed:', error); - return new Response(JSON.stringify({ error: 'Authentication failed' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); + Sentry.captureException(error, { extra: { url: request.url } }); + return Response.redirect(new URL('/sign-in?error=auth_failed', request.url)); }, }), }, @@ -438,11 +450,14 @@ export const Route = createFileRoute('/api/auth/callback')({ }); ``` +`onError` runs for every callback failure (missing `code`, state mismatch, token exchange failure, `onSuccess` throws). The SDK already emits a `console.error` for every failure, so if you wire Sentry's `console.error` ingestion you don't need to call `Sentry.captureException` yourself. + **Options:** -- `onSuccess?: (data) => Promise` - Called after successful authentication with user data, tokens, and authentication method -- `onError?: ({ error, request }) => Response` - Custom error handler that returns a Response -- `returnPathname?: string` - Override the redirect path after authentication (defaults to state or `/`) +- `onSuccess?: (data) => Promise` — Called after successful authentication with user data, tokens, and authentication method. +- `onError?: ({ error, request }) => Response` — Custom error handler that returns a Response. Errors thrown from inside `onError` are NOT caught by the SDK. +- `errorRedirectUrl?: string` — URL (absolute or relative) to redirect to on callback failure when `onError` is not set. If both are set, `onError` wins. Set this at route-construction time only — do not derive from request input (it would be an open-redirect vector). +- `returnPathname?: string` — Override the success-path redirect after authentication. Does not apply to errors. ### Client Hooks diff --git a/src/server/server.spec.ts b/src/server/server.spec.ts index b1f2cf6..795d12e 100644 --- a/src/server/server.spec.ts +++ b/src/server/server.spec.ts @@ -1,5 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getPKCECookieNameForState } from '@workos/authkit-session'; +import type { HandleCallbackOptions } from './types'; const SEALED_STATE = 'sealed-state-fixture'; const PKCE_COOKIE_NAME = getPKCECookieNameForState(SEALED_STATE); @@ -56,6 +57,8 @@ const successResult = (overrides: Record = {}) => ({ }); describe('handleCallbackRoute', () => { + let consoleErrorSpy: ReturnType; + beforeEach(() => { vi.clearAllMocks(); mockRedirectUriFromContext = undefined; @@ -66,6 +69,11 @@ describe('handleCallbackRoute', () => { createSignIn: mockCreateSignIn, clearPendingVerifier: mockClearPendingVerifier, }); + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); }); describe('missing code', () => { @@ -240,7 +248,6 @@ describe('handleCallbackRoute', () => { it('returns 500 with generic body on handleCallback failure', async () => { const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('Invalid code')); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const response = await handleCallbackRoute()({ request }); @@ -250,8 +257,6 @@ describe('handleCallbackRoute', () => { expect(body.error.description).toContain("Couldn't sign in"); expect(body.error).not.toHaveProperty('details'); expect(response.headers.getSetCookie().some((c) => c.startsWith(`${PKCE_COOKIE_NAME}=`))).toBe(true); - - consoleSpy.mockRestore(); }); it('calls onError with the underlying error and appends delete-cookie', async () => { @@ -264,7 +269,6 @@ describe('handleCallbackRoute', () => { headers: { 'X-Custom': 'preserved' }, }), ); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const response = await handleCallbackRoute({ onError })({ request }); @@ -273,8 +277,6 @@ describe('handleCallbackRoute', () => { expect(response.headers.get('X-Custom')).toBe('preserved'); expect(await response.text()).toBe('Custom error page'); expect(response.headers.getSetCookie().some((c) => c.startsWith(`${PKCE_COOKIE_NAME}=`))).toBe(true); - - consoleSpy.mockRestore(); }); it('reads verifier-delete header from clearPendingVerifier response when headers bag is empty', async () => { @@ -322,7 +324,6 @@ describe('handleCallbackRoute', () => { `http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`, ); mockGetAuthkitImpl = () => Promise.reject(new Error('Config missing')); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const response = await handleCallbackRoute()({ request }); @@ -334,8 +335,173 @@ describe('handleCallbackRoute', () => { expect(setCookies[1]).toContain('SameSite=None'); expect(setCookies[1]).toContain('Secure'); expect(setCookies.every((c) => c.includes('Max-Age=0'))).toBe(true); + }); + + it.each<{ label: string; arrange: () => Request; options?: HandleCallbackOptions }>([ + { + label: 'missing code', + arrange: () => new Request(`http://example.com/callback?state=${encodeURIComponent(SEALED_STATE)}`), + }, + { + label: 'getAuthkit throws', + arrange: () => { + mockGetAuthkitImpl = () => Promise.reject(new Error('Config missing')); + return new Request(`http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`); + }, + }, + { + label: 'handleCallback throws', + arrange: () => { + mockHandleCallback.mockRejectedValue(new Error('Token exchange failure')); + return new Request(`http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`); + }, + }, + { + label: 'onSuccess throws', + arrange: () => { + mockHandleCallback.mockResolvedValue(successResult()); + return new Request(`http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`); + }, + options: { + onSuccess: () => { + throw new Error('db failure'); + }, + }, + }, + ])('central console.error fires exactly once on $label', async ({ arrange, options }) => { + const request = arrange(); + await handleCallbackRoute(options ?? {})({ request }); + + const callbackErrorLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('OAuth callback failed'), + ); + expect(callbackErrorLogs).toHaveLength(1); + }); + + it('routes onSuccess throws through errorResponse with delete-cookies', async () => { + const request = new Request( + `http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockResolvedValue(successResult()); + const onSuccess = vi.fn(() => { + throw new Error('db failure'); + }); + + const response = await handleCallbackRoute({ onSuccess })({ request }); + + expect(response.status).toBe(500); + expect(response.headers.getSetCookie().some((c) => c.startsWith(`${PKCE_COOKIE_NAME}=`))).toBe(true); + }); + + it('does not catch errors thrown inside onError', async () => { + const request = new Request( + `http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('callback failure')); + const onError = vi.fn(() => { + throw new Error('user-side failure'); + }); + + await expect( + handleCallbackRoute({ onError, errorRedirectUrl: '/fallback' })({ request }), + ).rejects.toThrow('user-side failure'); + }); + }); + + describe('errorRedirectUrl path', () => { + it('returns 302 to absolute errorRedirectUrl with delete-cookies', async () => { + const request = new Request( + `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('boom')); + + const response = await handleCallbackRoute({ errorRedirectUrl: 'https://example.com/sign-in?error=auth' })({ + request, + }); + + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('https://example.com/sign-in?error=auth'); + expect(response.headers.getSetCookie().some((c) => c.startsWith(`${PKCE_COOKIE_NAME}=`))).toBe(true); + }); + + it('resolves relative errorRedirectUrl against request origin', async () => { + const request = new Request( + `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('boom')); - consoleSpy.mockRestore(); + const response = await handleCallbackRoute({ errorRedirectUrl: '/sign-in?error=auth_failed' })({ request }); + + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('http://example.com/sign-in?error=auth_failed'); + }); + + it('runs onError and ignores errorRedirectUrl when both are set', async () => { + const request = new Request( + `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('boom')); + const onError = vi.fn(() => new Response('custom', { status: 418 })); + + const response = await handleCallbackRoute({ onError, errorRedirectUrl: '/should-not-be-used' })({ request }); + + expect(onError).toHaveBeenCalledTimes(1); + expect(response.status).toBe(418); + expect(response.headers.get('Location')).toBeNull(); + + const callbackErrorLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('OAuth callback failed'), + ); + expect(callbackErrorLogs).toHaveLength(1); + }); + + it('falls back to JSON when errorRedirectUrl is malformed', async () => { + const request = new Request( + `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('boom')); + + const response = await handleCallbackRoute({ errorRedirectUrl: 'http://[::1' })({ request }); + + expect(response.status).toBe(500); + expect(response.headers.get('Content-Type')).toBe('application/json'); + const body = await response.json(); + expect(body.error.message).toBe('Authentication failed'); + expect(response.headers.getSetCookie().some((c) => c.startsWith(`${PKCE_COOKIE_NAME}=`))).toBe(true); + + const callbackLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('OAuth callback failed'), + ); + const malformedLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('errorRedirectUrl is malformed'), + ); + expect(callbackLogs).toHaveLength(1); + expect(malformedLogs).toHaveLength(1); + }); + + it('falls back to JSON 400 when errorRedirectUrl is malformed on missing-code path', async () => { + const request = new Request(`http://example.com/callback?state=${encodeURIComponent(SEALED_STATE)}`); + + const response = await handleCallbackRoute({ errorRedirectUrl: 'http://[::1' })({ request }); + + expect(response.status).toBe(400); + expect(response.headers.get('Content-Type')).toBe('application/json'); + }); + + it('ignores returnPathname on the error path', async () => { + const request = new Request( + `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, + ); + mockHandleCallback.mockRejectedValue(new Error('boom')); + + const response = await handleCallbackRoute({ + returnPathname: '/dashboard', + errorRedirectUrl: '/sign-in?error=auth', + })({ request }); + + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toContain('/sign-in?error=auth'); + expect(response.headers.get('Location')).not.toContain('/dashboard'); }); }); @@ -346,13 +512,10 @@ describe('handleCallbackRoute', () => { // Force the error path so errorResponse runs. `code=bad` with the mock // rejecting triggers the catch branch. mockHandleCallback.mockRejectedValue(new Error('boom')); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const res = await handleCallbackRoute()({ request }); const setCookies = res.headers.getSetCookie(); expect(setCookies.some((c) => c.startsWith(`${expected}=`))).toBe(true); - - consoleSpy.mockRestore(); }); it('emits no Set-Cookie delete when state is absent', async () => { diff --git a/src/server/server.ts b/src/server/server.ts index d4c585a..0a39c5d 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -105,8 +105,8 @@ async function handleCallbackInternal(request: Request, options: HandleCallbackO try { authkit = await getAuthkit(); - } catch (setupError) { - console.error('[authkit-tanstack-react-start] Callback setup failed:', setupError); + } catch { + // Swallowed: errorResponse below logs centrally when authkit is missing. } const url = new URL(request.url); @@ -148,7 +148,6 @@ async function handleCallbackInternal(request: Request, options: HandleCallbackO return new Response(null, { status: 307, headers }); } catch (error) { - console.error('OAuth callback failed:', error); return errorResponse(error, request, options, authkit, state, 500); } } @@ -161,6 +160,8 @@ async function errorResponse( state: string | null, defaultStatus: number, ): Promise { + console.error('[authkit-tanstack-react-start] OAuth callback failed:', error); + // Only the error path needs delete-cookie headers, so skip the // clearPendingVerifier round-trip on the happy path. const deleteCookieHeaders = await buildVerifierDeleteHeaders(authkit, state); @@ -176,6 +177,21 @@ async function errorResponse( }); } + if (options.errorRedirectUrl) { + try { + const target = new URL(options.errorRedirectUrl, request.url); + const headers = new Headers({ Location: target.toString() }); + for (const h of deleteCookieHeaders) headers.append('Set-Cookie', h); + return new Response(null, { status: 302, headers }); + } catch (urlError) { + console.error( + '[authkit-tanstack-react-start] errorRedirectUrl is malformed; falling back to JSON 500:', + urlError, + ); + // fall through to JSON + } + } + const headers = new Headers({ 'Content-Type': 'application/json' }); for (const h of deleteCookieHeaders) headers.append('Set-Cookie', h); return new Response( diff --git a/src/server/types.ts b/src/server/types.ts index d5259d5..3393c6f 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -11,7 +11,37 @@ export interface OauthTokens { export interface HandleCallbackOptions { returnPathname?: string; onSuccess?: (data: HandleAuthSuccessData) => void | Promise; + /** + * Custom error handler. Receives the underlying error and the original + * request, returns a Response. Errors thrown from inside `onError` are + * NOT caught by the SDK — they propagate up to the runtime. Wrap your + * `onError` body in a try/catch if you want different behavior. + * + * If both `onError` and `errorRedirectUrl` are provided, `onError` wins + * and `errorRedirectUrl` is ignored. + */ onError?: (params: { error?: unknown; request: Request }) => Response | Promise; + /** + * Optional URL to redirect the user to when the callback fails. Accepts + * absolute URLs (`https://example.com/sign-in`) or relative paths + * (`/sign-in?error=auth_failed`); relative values resolve against the + * request origin. + * + * When set and `onError` is not, the SDK responds with a 302 Location + * redirect plus the verifier-delete cookies. When `onError` is also + * set, this option is ignored. + * + * The redirect URL is set at route-construction time by application + * code, not derived from request input. Do not pass user-controlled + * values here. The SDK does not validate the URL scheme; any value the + * URL constructor accepts is accepted (including `javascript:` and + * `data:`). + * + * If the value is malformed and the URL constructor throws, the SDK + * logs a config warning and falls back to the path-dependent JSON + * error response (400 or 500) with delete-cookies. + */ + errorRedirectUrl?: string; } export interface HandleAuthSuccessData { From e52fa6685d66e88e926b6342c1869f1ea492f6a1 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 09:06:30 -0500 Subject: [PATCH 2/3] chore: formatting --- src/server/server.spec.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/server/server.spec.ts b/src/server/server.spec.ts index 795d12e..925186a 100644 --- a/src/server/server.spec.ts +++ b/src/server/server.spec.ts @@ -402,17 +402,15 @@ describe('handleCallbackRoute', () => { throw new Error('user-side failure'); }); - await expect( - handleCallbackRoute({ onError, errorRedirectUrl: '/fallback' })({ request }), - ).rejects.toThrow('user-side failure'); + await expect(handleCallbackRoute({ onError, errorRedirectUrl: '/fallback' })({ request })).rejects.toThrow( + 'user-side failure', + ); }); }); describe('errorRedirectUrl path', () => { it('returns 302 to absolute errorRedirectUrl with delete-cookies', async () => { - const request = new Request( - `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, - ); + const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('boom')); const response = await handleCallbackRoute({ errorRedirectUrl: 'https://example.com/sign-in?error=auth' })({ @@ -425,9 +423,7 @@ describe('handleCallbackRoute', () => { }); it('resolves relative errorRedirectUrl against request origin', async () => { - const request = new Request( - `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, - ); + const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('boom')); const response = await handleCallbackRoute({ errorRedirectUrl: '/sign-in?error=auth_failed' })({ request }); @@ -437,9 +433,7 @@ describe('handleCallbackRoute', () => { }); it('runs onError and ignores errorRedirectUrl when both are set', async () => { - const request = new Request( - `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, - ); + const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('boom')); const onError = vi.fn(() => new Response('custom', { status: 418 })); @@ -456,9 +450,7 @@ describe('handleCallbackRoute', () => { }); it('falls back to JSON when errorRedirectUrl is malformed', async () => { - const request = new Request( - `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, - ); + const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('boom')); const response = await handleCallbackRoute({ errorRedirectUrl: 'http://[::1' })({ request }); @@ -489,9 +481,7 @@ describe('handleCallbackRoute', () => { }); it('ignores returnPathname on the error path', async () => { - const request = new Request( - `http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`, - ); + const request = new Request(`http://example.com/callback?code=invalid&state=${encodeURIComponent(SEALED_STATE)}`); mockHandleCallback.mockRejectedValue(new Error('boom')); const response = await handleCallbackRoute({ From f422dd5ae521a3bcbb51c90d3f3b425544d013d2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:12:23 +0000 Subject: [PATCH 3/3] Fix swallowed setup error and hardcoded status in log 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 --- src/server/server.spec.ts | 22 ++++++++++++++++++++++ src/server/server.ts | 9 +++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/server/server.spec.ts b/src/server/server.spec.ts index 925186a..d5fa597 100644 --- a/src/server/server.spec.ts +++ b/src/server/server.spec.ts @@ -337,6 +337,22 @@ describe('handleCallbackRoute', () => { expect(setCookies.every((c) => c.includes('Max-Age=0'))).toBe(true); }); + it('logs the original setup error when getAuthkit() rejects', async () => { + const originalError = new Error('Config missing'); + mockGetAuthkitImpl = () => Promise.reject(originalError); + const request = new Request( + `http://example.com/callback?code=auth_123&state=${encodeURIComponent(SEALED_STATE)}`, + ); + + await handleCallbackRoute()({ request }); + + const callbackErrorLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('OAuth callback failed'), + ); + expect(callbackErrorLogs).toHaveLength(1); + expect(callbackErrorLogs[0]![1]).toBe(originalError); + }); + it.each<{ label: string; arrange: () => Request; options?: HandleCallbackOptions }>([ { label: 'missing code', @@ -478,6 +494,12 @@ describe('handleCallbackRoute', () => { expect(response.status).toBe(400); expect(response.headers.get('Content-Type')).toBe('application/json'); + + const malformedLogs = consoleErrorSpy.mock.calls.filter( + (args) => typeof args[0] === 'string' && args[0].includes('errorRedirectUrl is malformed'), + ); + expect(malformedLogs).toHaveLength(1); + expect(malformedLogs[0]![0]).toContain('falling back to JSON 400'); }); it('ignores returnPathname on the error path', async () => { diff --git a/src/server/server.ts b/src/server/server.ts index 0a39c5d..502de52 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -103,10 +103,11 @@ async function buildVerifierDeleteHeaders( async function handleCallbackInternal(request: Request, options: HandleCallbackOptions): Promise { let authkit: Awaited> | undefined; + let setupError: unknown; try { authkit = await getAuthkit(); - } catch { - // Swallowed: errorResponse below logs centrally when authkit is missing. + } catch (error) { + setupError = error; } const url = new URL(request.url); @@ -117,7 +118,7 @@ async function handleCallbackInternal(request: Request, options: HandleCallbackO return errorResponse(new Error('Missing authorization code'), request, options, authkit, state, 400); } if (!authkit) { - return errorResponse(new Error('AuthKit not initialized'), request, options, authkit, state, 500); + return errorResponse(setupError ?? new Error('AuthKit not initialized'), request, options, authkit, state, 500); } try { @@ -185,7 +186,7 @@ async function errorResponse( return new Response(null, { status: 302, headers }); } catch (urlError) { console.error( - '[authkit-tanstack-react-start] errorRedirectUrl is malformed; falling back to JSON 500:', + `[authkit-tanstack-react-start] errorRedirectUrl is malformed; falling back to JSON ${defaultStatus}:`, urlError, ); // fall through to JSON