Repository navigation
Sweep the unread-body reset out of the wheel verifier's own client - #42
Draft
Deep-Sixed wants to merge 1 commit into
Draft
Deep-Sixed wants to merge 1 commit into
Deep-Sixed wants to merge 1 commit into
Conversation
PR #39 and #40 fixed this race in `Client` in src/system/tests/test_web_surface.py and stopped there; `ops/tools/verify_wheel.py` carries its own `ask()` helper, and the AST pass #40 relied on covered the test client rather than this file. The surface settles several refusals on the envelope -- provenance, media type, declared length, declared source -- before a byte of the body is read. A body actually sent at one of those addresses stays unread in the receive buffer, and closing on unread bytes is a reset rather than a clean shutdown on Windows, so whether the refusal or the reset arrives first is a race. It surfaced once as a `ConnectionAbortedError` on the 415 check under verify (windows-latest, 3.14). `ask()` now speaks http.client instead of urllib, because `urlopen` has no way to declare a body and withhold it and no tolerance for the reset either. `transmit` sends the headers and none of the body; `length` declares something other than the truth. Both mirror `Client.send`, so the two clients now hold the same controls for the same reason. Eleven call sites were sending a body to a refusal settled before the read, not just the 415: Origin and token on the status, decision and outward commands; both intake addresses on a launch with no evaluation profile; and the missing and blank source declarations. Each declares its body and sends none. The 413 check, already declaring rather than sending, now declares against the real message instead of a placeholder byte. Verified mechanically rather than by reading: with the handler instrumented to record whether each POST read its body, no request sends one to an address that refuses before reading, and no withheld body belongs to an address that reads -- a withheld body there would hang to the timeout rather than pass quietly. Gates: pytest (1686 passed), ruff check, ruff format --check, verify_tree, verify_secrets, verify_wheel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZrRh6P6mTsdapYFzsGF5a
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.
What this is
The same unread-body reset race that #39 and #40 fixed in
Clientinsrc/system/tests/test_web_surface.py, at the call sitesops/tools/verify_wheel.pyowns through its ownask()helper. #40's AST pass over "all 112 client call sites" covered the test client; this file has a second one.The failure
Seen once on
verify (windows-latest, 3.14)for79d2864— nondeterministic, not a hard break:Handler._ingest_emlrefuses a wrong media type on the envelope, before the body is read — its docstring says so and that ordering is the property the check is about. So the sent body stays unread in the receive buffer, closing on unread bytes is a reset rather than a clean shutdown on Windows, and whether the415or the reset reaches the client first is a race.urlopenhas no tolerance for it, unlikeClient.unread.What changed
ask()speakshttp.clientrather thanurllib, becauseurlopencannot declare a body and withhold it. It gains the two controlsClient.sendalready holds, for the same reason and with that written down beside them:transmit=False— send the headers and none of the body.length— declare something other than the truth, the only way to reach the size ceiling without writing at it.The 415 was not the only one. Eleven call sites were sending a body to a refusal the surface settles before reading:
403/401_command, before a byte is read409_ingest_eml/_ingest_gmail, launch capability first400declared_namespace, before_materialapplication/json415Each now declares its body and sends none. The
413check, already declaring rather than sending, drops its placeholder byte and declaresMAX_MESSAGE_BYTES + 1against the real message.Verification
Checked mechanically rather than by reading, since reading is what missed it last time. With
Handler.do_POSTinstrumented to record whether each request read its body, every POST the verifier makes falls into one of three shapes:Nothing lands in the fourth shape — body sent, never read — which is the one that resets. The instrumentation is scratch-only and not part of this diff. It also rules out the opposite mistake: withholding a body an address does read hangs to the timeout rather than passing quietly, so the run completing is itself the check.
Gates, all locally on Linux:
The Windows race is not reproducible here by construction; what this PR establishes locally is that no request sends a body the surface will not read, which is the condition the reset needs.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SZrRh6P6mTsdapYFzsGF5a
Generated by Claude Code