Skip to content

Sweep the unread-body reset out of the wheel verifier's own client - #42

Draft
Deep-Sixed wants to merge 1 commit into
mainfrom
claude/epic-feynman-qlqbvk
Draft

Deep-Sixed wants to merge 1 commit into
mainfrom
claude/epic-feynman-qlqbvk

Conversation

@Deep-Sixed

Copy link
Copy Markdown
Owner

What this is

The same unread-body reset race that #39 and #40 fixed in Client in src/system/tests/test_web_surface.py, at the call sites ops/tools/verify_wheel.py owns through its own ask() 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) for 79d2864 — nondeterministic, not a hard break:

File "...\careersignal-wheel-k33nc9zr\web_check.py", line 288, in <module>
    assert ask(
        "/api/v1/intake/eml", method="POST", body=WHEEL_MESSAGE, media="application/json",
        headers=DECLARED,
    )[0] == 415
ConnectionAbortedError: [WinError 10053] An established connection was aborted by the
software in your host machine

Handler._ingest_eml refuses 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 the 415 or the reset reaches the client first is a race. urlopen has no tolerance for it, unlike Client.unread.

What changed

ask() speaks http.client rather than urllib, because urlopen cannot declare a body and withhold it. It gains the two controls Client.send already 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:

where refusal settled by
status, decision, draft, reconcile (6 calls) 403 / 401 _command, before a byte is read
both intake addresses on a launch with no evaluation profile 409 _ingest_eml / _ingest_gmail, launch capability first
intake with no declared source, and with a blank one 400 declared_namespace, before _material
intake as application/json 415 the media check — the reported failure

Each now declares its body and sends none. The 413 check, already declaring rather than sending, drops its placeholder byte and declares MAX_MESSAGE_BYTES + 1 against the real message.

Verification

Checked mechanically rather than by reading, since reading is what missed it last time. With Handler.do_POST instrumented to record whether each request read its body, every POST the verifier makes falls into one of three shapes:

  • no body, nothing unread;
  • body sent and read;
  • body declared, withheld, never read.

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:

uv run pytest -q                        1686 passed
uv run ruff check .                     All checks passed!
uv run ruff format --check .            77 files already formatted
uv run python ops/tools/verify_tree.py  PASS
uv run python ops/tools/verify_secrets.py  PASS
uv run python ops/tools/verify_wheel.py    PASS

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

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
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