Skip to content

fix(rate-limit): answer with the JSON error shape rather than plain text - #244

Merged
Bccorb merged 1 commit into
mainfrom
fix/json-rate-limit-response
Aug 31, 2026
Merged

fix(rate-limit): answer with the JSON error shape rather than plain text#244
Bccorb merged 1 commit into
mainfrom
fix/json-rate-limit-response

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #241.

Nine sites, not three

The issue named the three limiters that set message: 'Too many requests, please try again later'. The other six set no message at all and inherited express-rate-limit's own string default, so they answered plain text for the same reason without saying so anywhere. Grep found a third of the problem.

Verified against the library rather than assumed:

message Response
unset (library default) 429 text/html Too many requests, please try again later.
a string 429 text/html Too many requests, please try again later
an object 429 application/json {"error":"..."}

All nine, including the JWKS limiter, now send the object and answer { "error": "Too many requests, please try again later" }.

The slow-down message was unreachable

slowDown.ts also carried the string. express-slow-down replaces the handler with one that only computes a delay and calls next, so it never answers a request and that option could never be read. Removed rather than converted, since converting dead config to different dead config is worse than deleting it.

Contract change and blast radius

The 429 body and content type change. Surveyed before committing, and both known consumers were coping with the text form rather than depending on it:

  • @seamless-auth/core: authFetch.ts carries a makeJsonTolerant shim whose comment names this exact case. Every adapter response passes through it because of this one endpoint.
  • seamless-auth-react: seamless-auth-react#41, a non-JSON 429 crashing the client.
  • seamless-auth-admin-dashboard: maps 429 to fixed wording and never surfaces upstream text. Unaffected.

Nothing depended on the text, so no coordinated release is needed. Whether makeJsonTolerant can now be simplified is being looked at separately; it also covers 204, so it is not a straight deletion.

Tests

  • The integration spec added in test: exercise the global rate limiters as mounted middleware #238 pinned the plain-text body deliberately, with a comment saying to change it only with coordination. That is this change, so the assertion is now the JSON shape.
  • A new unit test asserts the body across all eight constructed limiters at once, rather than per limiter, precisely because six of them were wrong by omission and a per-site assertion is what missed them.
  • Another asserts the slow-down sets no message, so nobody adds one back to something that cannot answer.

Verification

1216 tests passing, 1 skipped. Typecheck, lint, format clean. minor changeset per the pre-1.0 rule, describing the behaviour change. docs/api-contract.md updated: it claimed every 4xx and 5xx used one shape, which was not quite true until now.

Closes #241.

Every other 4xx and 5xx on this API is { error }. A 429 was the exception:
express-rate-limit sends a string message through res.send, which lands as
text/html, so a client parsing error bodies as JSON got a parse failure.

Grepping for the string found three of the nine sites. The other six set no
message at all and inherited the library's own string default, so they were
plain text for the same reason without saying so. All nine now send an object.

The message on the slow-down is removed rather than converted.
express-slow-down replaces the handler with one that only delays and calls
next, so it never answers a request and that option could never be read.

Two consumers were already coping with the text form: @seamless-auth/core has
a makeJsonTolerant shim in authFetch that names this case, and
seamless-auth-react was crashed by it once. The admin dashboard maps 429 to
fixed wording and never surfaces upstream text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(rate-limit): a 429 answers plain text, not the JSON error shape

1 participant