Answer plain text when the caller asks for no particular format - #10
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
docs/API.mddocuments the compilation endpoints as returning plain text unless the caller asks for JSON viaAccept, and that is what they do when served directly —CompileHandler.parseRequestusesreq.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 noAcceptat all.Same request, two environments, the only difference being whether the router is in the path:
So enabling the router for an environment silently changes a documented response format for every client that does not set
Accept—curlwith no flags being the obvious one. Browsers are unaffected, because jQuery sendsdataType: '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.createSuccessResponsetakes the decision rather than the header, so the two cannot drift apart again.Verified the semantics being matched rather than assuming them:
Acceptreq.accepts(['text','json'])text*/*texttext/plaintextapplication/jsonjsonapplication/json, text/javascript, */*; q=0.01(jQuery)jsontext/htmlfalse→ textTests
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,
typecheckandbiomeclean.Found by
ce ce-router smokein 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