Skip to content

docs(openapi): no route declares a 429, so the generated client does not know it can happen #245

Description

@Bccorb

Fell out of #241, closed by #244. Now that a 429 answers the documented { error } shape, the prose contract is complete but the machine-readable one is not.

openapi.json contains zero "429" responses. Meanwhile 17 route definitions attach a per-flow limiter (magicLinkIpLimiter, otpIpLimiter, oauthIpLimiter, and their identity-keyed siblings), and every route is additionally behind the global dynamicRateLimit. So a 429 is reachable on effectively every endpoint and documented on none.

Two consequences:

  • The generated client is wrong by omission. src/generated/api.ts is emitted from the spec and committed, so a consumer reading it, or anyone generating their own client, gets a response union with no 429 in it. They will write code that cannot see the case.
  • It reads as an undocumented failure mode. For a repo heading into certification where the OpenAPI document is a deliverable, "every endpoint can return a status the spec never mentions" is the kind of gap an assessor asks about, and the answer today is that it was never wired up rather than that it was considered.

Why it happens

Limiters are Express middleware attached via middleware: [...] in the route definition. defineRoute builds OpenAPI responses only from schemas.response, which handlers declare, so nothing about a middleware-produced status reaches the registry. Note the same is already true of the 403 the CORS handler produces and the 500 from the top-level error handler, so this is one instance of a general shape rather than a one-off.

Scope

Decide where the truth should live, then wire it:

  • Simplest: have defineRoute add a 429: ErrorSchema to every route it registers, the way it already injects the 400 ValidationErrorSchema for any route that validates a request. That precedent exists and is the closest fit.
  • Or: add it only to routes that carry a limiter, which is more precise but leaves the global limiter undocumented, and the global one applies to everything.

The first is more honest about what a caller can actually receive. Regenerate openapi.json and src/generated/api.ts with npm run generate:api afterwards; tests/unit/openapi/generatedContract.spec.ts fails if they drift.

Worth considering the same treatment for the CORS 403 and the unhandled-error 500 (#230, closed by #239) in the same pass, since they have the identical cause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions