Skip to content

Answer plain text when the caller asks for no particular format - #10

Merged
partouf merged 1 commit into
mainfrom
fix-accept-default
Sep 22, 2026
Merged

partouf merged 1 commit into
mainfrom
fix-accept-default

Conversation

@partouf

@partouf partouf commented Sep 22, 2026

Copy link
Copy Markdown
Member

The bug

docs/API.md documents the compilation endpoints as returning plain text unless the caller asks for JSON via Accept, and that is what they do when served directly — CompileHandler.parseRequest uses req.accepts(['text', 'json']), which resolves an absent or */* Accept to the first entry, text.

This service decided instead by testing whether the raw header contained text/plain, so anything not naming that type got JSON — including a caller that sent no Accept at all.

Same request, two environments, the only difference being whether the router is in the path:

prod (served directly)   content-type: text/plain; charset=utf-8
beta (through here)      content-type: application/json; charset=utf-8

So enabling the router for an environment silently changes a documented response format for every client that does not set Accept — curl with no flags being the obvious one. Browsers are unaffected, because jQuery sends dataType: 'json' and therefore always asked for JSON explicitly. That is why it went unnoticed.

The fix

Negotiation is now Express's own req.accepts(['text', 'json']) — the same call the endpoint makes when served directly — rather than a second implementation that has to be kept in step with it. createSuccessResponse takes the decision rather than the header, so the two cannot drift apart again.

Verified the semantics being matched rather than assuming them:

Accept req.accepts(['text','json'])
(absent) text
*/* text
text/plain text
application/json json
application/json, text/javascript, */*; q=0.01 (jQuery) json
text/html false → text

Tests

The integration tests asserted a JSON body while sending no Accept, so they were pinning the old behaviour. They now ask for JSON the way a client wanting JSON would, and a new test in each file covers the default itself — so both the opt-in and the default are pinned rather than only one.

129 tests pass, typecheck and biome clean.

Found by

ce ce-router smoke in the infra repo, which runs the same checks against a direct-routed environment as a control — this failed on beta and passed on prod, which is what identified it as router-introduced rather than pre-existing.

🤖 Generated with Claude Code

docs/API.md documents the compilation endpoints as returning plain text unless
the caller asks for JSON via Accept, and that is what they do when served
directly: CompileHandler uses req.accepts(['text', 'json']), which resolves an
absent or */* Accept to the first entry, text.

This service decided instead by testing whether the raw header contained
'text/plain', so anything that did not name that type got JSON - including a
caller that sent no Accept at all. Measured against the same request:

    prod (served directly)  content-type: text/plain; charset=utf-8
    beta (through here)     content-type: application/json; charset=utf-8

So routing an environment through this service silently changed a documented
response format for every client that does not set Accept, curl with no flags
being the obvious one. Browsers are unaffected: jQuery sends
dataType: 'json', so the frontend always asked for JSON explicitly, which is
why this went unnoticed.

Negotiation is now Express's own req.accepts(['text', 'json']), the same call
the endpoint makes when served directly, rather than a second implementation
that has to be kept in step. createSuccessResponse takes the decision rather
than the header, so it cannot drift again.

The integration tests asserted a JSON body while sending no Accept, so they
were pinning the old behaviour; they now ask for JSON as a client wanting JSON
would. A new test covers the default itself, in both files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@partouf
partouf merged commit 4933a1d into main Sep 22, 2026
2 checks passed
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.

1 participant