Skip to content

getCookiesFromHandshake swallows nonce handshake-payload fetch failures — resolves as generic signed-out #9114

Description

@amitosdev

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.error in 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)

  1. Any SvelteKit/Next.js/etc. app using @clerk/backend's authenticateRequest on a production Clerk instance, deployed so that the Backend API call can fail — simplest forcing function: an invalid CLERK_SECRET_KEY at the point createClerkClient runs (that is exactly what the svelte-clerk bug produced).
  2. Send any request with a handshake nonce cookie: curl https://your-app.example/ -H 'Cookie: __clerk_handshake_nonce=anything'.
  3. Observed: request resolves as generic signed-out (reason: session-token-missing, empty message); the only evidence of the real failure is Clerk: HandshakeService: error getting handshake payload: … on the server console.
  4. Expected: the returned requestState carries 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)

  1. Surface the failure distinctly. Return/propagate a dedicated auth error reason (e.g. AuthErrorReason.HandshakePayloadFetchFailed) instead of the generic session-token-missing, so requestState consumers and framework adapters can log or act on it. Even keeping the signed-out outcome, a distinguishable reason/message would have turned a multi-day investigation into a five-minute log read.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions