getCookiesFromHandshake swallows nonce handshake-payload fetch failures — resolves as generic signed-out
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.8k
- Forks
- 472
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 189
Description
Note for triage: filed at the request of Clerk support (Ryan) from an existing support ticket, where the root cause and evidence below were confirmed. This is an SDK hardening/observability request backed by a production incident, not a help request — hence no separate minimal-repro repo; deterministic repro steps are inline below.
Package: @clerk/backend (observed on 3.8.3; code is current on main)
Summary
When a nonce-format handshake's payload fetch fails, HandshakeService.getCookiesFromHandshake() (packages/backend/src/tokens/handshake.ts) catches the error, logs it with console.error, and resolves with zero cookie directives. resolveHandshake() then finds no session cookie and returns a plain signedOut(reason: AuthErrorReason.SessionTokenMissing, message: "").
The result: a server-side infrastructure failure (Backend API unreachable, misconfigured/missing secret key, transient 5xx) is laundered into an ordinary "signed out" auth state. The handshake "succeeds" empty, auth never converges, and nothing distinguishable surfaces to the adapter, middleware, or requestState consumer — the only trace is a console line in server logs.
// getCookiesFromHandshake()
if (this.authenticateContext.handshakeNonce) {
try {
const handshakePayload = await this.authenticateContext.apiClient?.clients.getHandshakePayload({
nonce: this.authenticateContext.handshakeNonce
});
if (handshakePayload) {
cookiesToSet.push(...handshakePayload.directives);
}
} catch (error) {
console.error('Clerk: HandshakeService: error getting handshake payload:', error);
}
}
Note the apiClient?. optional chaining has the same property: a missing apiClient silently yields no directives with no log at all.
Real-world impact
This made a production outage take days to diagnose. svelte-clerk constructed its Backend API client with an undefined secret key on Vercel (module-scope env read racing SvelteKit's Server.init() — fixed in wobsoriano/svelte-clerk#442). Every nonce handshake threw Missing Clerk Secret Key locally, before any request reached Clerk's API:
- Flows depending on server-side handshake convergence (Dashboard impersonation) dead-looped to the sign-in page with zero surfaced errors.
- Clerk support searched API logs for our handshake nonces and found nothing — because the SDK threw before the network call. That "zero requests" observation was only reconcilable once we found the swallowed
console.errorin Vercel function logs. - From the app's perspective the symptom was "auth randomly won't converge," which points at cookies, tokens, and clock skew — everywhere except the actual failure.
Context confirmed with Clerk support (Ryan) in an existing ticket; happy to link it internally.
Reproduction (deterministic, no impersonation needed)
- Any SvelteKit/Next.js/etc. app using
@clerk/backend'sauthenticateRequeston a production Clerk instance, deployed so that the Backend API call can fail — simplest forcing function: an invalidCLERK_SECRET_KEYat the pointcreateClerkClientruns (that is exactly what the svelte-clerk bug produced). - Send any request with a handshake nonce cookie:
curl https://your-app.example/ -H 'Cookie: __clerk_handshake_nonce=anything'. - Observed: request resolves as generic signed-out (
reason: session-token-missing, empty message); the only evidence of the real failure isClerk: HandshakeService: error getting handshake payload: …on the server console. - Expected: the returned
requestStatecarries a distinguishable reason (and/or the SDK attempts a safe fallback) so the adapter/app can tell "infrastructure failure during handshake resolution" apart from "user is signed out."
Suggested hardening (either or both)
- Surface the failure distinctly. Return/propagate a dedicated auth error reason (e.g.
AuthErrorReason.HandshakePayloadFetchFailed) instead of the genericsession-token-missing, sorequestStateconsumers and framework adapters can log or act on it. Even keeping the signed-out outcome, a distinguishablereason/messagewould have turned a multi-day investigation into a five-minute log read. - Fall back where safe. When the nonce payload fetch fails, fall back to a token-format handshake if the conditions allow, so a transient BAPI failure doesn't cost the user their session convergence.
Swallowing may be a deliberate availability choice (don't hard-fail requests on BAPI hiccups) — that's compatible with suggestion 1: keep resolving, but make the failure observable in the returned state, not only stderr.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/backend/src/tokens/handshake.ts, reading HandshakeService.getCookiesFromHandshake() and resolveHandshake(), then reproduce the failure with an invalid CLERK_SECRET_KEY and a handshake nonce cookie. Trace how the result reaches requestState and define a distinguishable handshake failure or safe fallback; done means consumers can tell infrastructure failure apart from an ordinary signed-out state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100