fix(server): a 204 leaves with no body, whoever put one there - #1
Open
MichaelC8E wants to merge 1 commit into
Open
MichaelC8E wants to merge 1 commit into
MichaelC8E wants to merge 1 commit into
Conversation
RFC 9110 s15.3.5 forbids content on a 204. The toolbar guard one commit back stops one injector from writing it; two cases stay open, and both fail on untouched v3 as well, so that guard introduces neither: - a handler that passes content alongside a 204 keeps it - a HEAD on a 204 reports the Content-Length of a body that may not exist The feedback widget in app() reads the same text/html content type from the far side of handle(), so it is reachable the same way. _stage_no_content_strip empties the body for any 204, the way _stage_head_strip already does for HEAD - "even for an explicit Router.head() handler that accidentally returned one". A guard inside one injector protects one injector. Position is behaviour twice over. Before _stage_dev_inspector_capture, or the dev dashboard records 1037 bytes for a response that ships none - the stage list's own comment says body_size is meant to be what went on the wire. Before _stage_head_strip, or a HEAD on a 204 carries that forbidden Content-Length. Both orderings have a test. tests/test_204_no_body_conformance.py, 9 cases driven through the real ASGI app and modelled on its HEAD sibling: 7 fail on untouched v3, 2 fail with the toolbar guard alone, all 9 pass here. The stage-order contract in test_dispatch_pipeline.py is updated to match.
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.
Builds on your 204 guard rather than replacing it — merge this into
fix/content-injection-issueand tina4stack#132 picks it up automatically.Your diagnosis is confirmed by measurement. With
TINA4_DEBUG=true, a scaffoldedresponse(None, 204)went out with 1037 bytes of toolbar andContent-Length: 1037; anOPTIONSon a known path went out with 1032. Under uvicorn'sh11that is refused outright —Too much data for declared Content-Length— and the connection is torn down, so the next request over it dies withRemoteDisconnectedwhile the DELETE that caused it still returned a clean 204. Underhttptools, and under the built-in dev bridge, the bytes simply go out and are discarded; withAccept-Encoding: gzipthe 204 even goes out gzip-encoded, because the toolbar pushes the body past the compression threshold inbuild_headers.What this adds
Your guard stops the toolbar writing to a 204. Two cases stay open, and both fail on untouched
v3as well, so your change introduces neither:response("<b>x</b>", 204)still ships 39 bytesHEADon a 204 reports theContent-Lengthof a body that may not existinject_feedback_widgetinapp()reads the sametext/htmlcontent type from the far side ofhandle(), so it is reachable the same way._stage_no_content_stripempties the body for any 204, the way_stage_head_stripalready does for HEAD — "even for an explicitRouter.head()handler that accidentally returned one". A guard inside one injector protects one injector.The position is behaviour, twice
_stage_dev_inspector_capture, or the dev dashboard recordsbody_size: 1037for a response that ships none. The stage list's own comment saysbody_sizeis meant to be what went on the wire. (This was a real defect in the first draft of this change, found by moving it and looking.)_stage_head_strip, or aHEADon a 204 carries that forbiddenContent-Length.Both orderings have a test.
Tests
tests/test_204_no_body_conformance.py— 9 cases driven through the real ASGI app, modelled on its siblingtest_head_no_body_conformance.py:v3Full suite on this branch: 13 failed, 5251 passed, 680 skipped — the same 13 as the untouched baseline on this machine (database-timeout, port-takeover, queue-validation and session-backend tests that need services this box does not run), and the 9 extra passes are this file. 9 of 9 mutations of the new stage are caught by the behaviour tests alone;
test_dispatch_pipelinewas excluded from that run on purpose, since it pins the stage list and would catch every reorder by construction.The stage-order contract in
test_dispatch_pipeline.pyis updated to match, with the reason in its assertion message.Also worth knowing
tina4 servenever reaches the uvicorn failure:run()only consults_find_production_server()when debug is off, and the toolbar only injects when debug is on. Hitting it needsTINA4_DEBUG=trueplus an externally-runuvicorn asgi:app. Worth checking which one you were running when you first saw the DELETE fail — the symptom differs quite a bit between them.The same missing status check exists in php (
Tina4/Router.phpinjectDevToolbar), ruby (lib/tina4/dispatch_pipeline.rbdev_toolbar_inject) and nodejs (packages/core/src/server.tsisInjectableHtml). Read only — none reproduced yet.