Skip to content

[Bug]: Request validation answers 400 with a raw ZodError, violating the route's declared ErrorSchema #222

Description

@Bccorb

Split out of #183, where this surfaced while building the client half
(fells-code/seamless-auth-react#137).

What happened

A request-validation failure in defineRoute answers 400 with the raw
ZodError, which does not match the 400: ErrorSchema the route declares and
carries no machine-readable error field for a client to branch on.

src/lib/defineRoute.ts:

if (query) {
  req.query = query.parse(req.query) as typeof req.query;
}
...
} catch (error: unknown) {
  return res.status(400).json(error);
}

GET /webauthn/register/start?attachment=bogus is the case I hit.
WebAuthnRegisterStartQuerySchema rejects it, so registerWebAuthn never runs
and the AAGUID and policy logic is never reached.

Serialized body, confirmed against the pinned zod@4.3.6:

{
  "name": "ZodError",
  "message": "[\n  {\n    \"code\": \"invalid_value\",\n    \"values\": [\n      \"platform\",\n      \"cross-platform\"\n    ],\n    \"path\": [\n      \"attachment\"\n    ],\n    \"message\": \"Invalid option: expected one of \\\"platform\\\"|\\\"cross-platform\\\"\"\n  }\n]"
}

message is a JSON-encoded string of the issues array, and there is no error
key at all.

Why it matters

ErrorSchema is { message?: string, error: string }, where error is
required. This body has no error, so the route violates the contract it
publishes for itself, and any consumer generated from or validated against that
contract is wrong about this response.

The response-schema check does not catch it. validate is pushed onto the
middleware stack before wrappedHandler, so when validation fails the handler
never runs and the res.json override that would have logged the mismatch is
never installed. Nothing reports the drift.

For a client the practical result is worse than a missing code. In
@seamless-auth/react, extractMessage reads body.error, then falls back to
body.message. With no error present it takes message, so registerPasskey()
resolves with error.message set to the entire escaped JSON blob above. An app
rendering error.message, which is the documented fallback for an unrecognised
failure, puts that in front of a user. Verified against the SDK's own
toSeamlessAuthError, not inferred.

This is not specific to attachment. validate is shared, so every route with
a params, query or body schema answers a malformed request this way.
attachment is just where it became reachable, because the SDK now sends the
parameter.

Steps to reproduce

  1. GET /webauthn/register/start?attachment=bogus with a valid ephemeral token
  2. Observe 400 with the body above
  3. Note the absent error key, against the route's declared 400: ErrorSchema

Suggested fix

InvalidPayloadResponseSchema in @seamless-auth/types already describes the
right shape and is not currently used here:

{ error: z.string(), details: z.unknown().optional() }

Mapping the catch onto it, with a stable code in error and the Zod issues under
details, would satisfy the declared contract and give clients something to
branch on. The React SDK already reads a nested details for exactly this reason
(a proxy normalising the body), so that shape needs no client change.

Worth deciding separately how much of the Zod issue detail should reach an
unauthenticated caller. Echoing raw validator output describes the server's
internal schema, and the existing comment on the response-validation catch
already takes the position that schema issues should not leak to the client.
The same reasoning applies here.

Not fixed from the client

@seamless-auth/react types the option as a union, so an unrecognised value is
unrepresentable for a TypeScript caller and the SDK will not produce this
request. That is a guard rail, not a fix: any other client, or a JavaScript
caller, still hits it.

Correction

My earlier comment on #183 said response-schema validation logs this mismatch.
That was wrong, for the middleware-ordering reason above. Nothing logs it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions