Skip to content

Fix CodeUnits write ambiguity + 500 fallback after deferred startwrite (#1302, #1303)#1305

Merged
quinnj merged 3 commits into
masterfrom
jq-stream-error-fixes
Jun 12, 2026
Merged

Fix CodeUnits write ambiguity + 500 fallback after deferred startwrite (#1302, #1303)#1305
quinnj merged 3 commits into
masterfrom
jq-stream-error-fixes

Conversation

@quinnj

@quinnj quinnj commented Jun 12, 2026

Copy link
Copy Markdown
Member

Fixes #1302, fixes #1303 (both reported by @PingoLee — thanks for the unusually precise root-cause analysis on both).

#1302write(::Stream, ::Base.CodeUnits) ambiguity

Base.CodeUnits <: DenseVector{UInt8}, so Base's write(::IO, ::Base.CodeUnits) was ambiguous with the StridedVector{UInt8} methods on Stream (and on the internal _RequestDeadlineWriteIO). Added disambiguating CodeUnits{UInt8} methods delegating to the existing byte paths. Verified through a real handler (write(stream, codeunits("ok")) → client receives ok) and a client HTTP.open body write.

#1303 — handler throw after startwrite silently dropped the connection

Exactly as the issue diagnosed: h1 FIXED mode defers the response head at startwrite while flipping response_started, and the error path gated its 500 fallback on !response_started — so a post-startwrite throw tore down the connection with zero bytes on the wire (ParseError: unexpected EOF client-side).

Fix: a new head_committed flag on Stream, set only at the two places head bytes actually reach the transport (h1 buffered write, h2 HEADERS frame). The h1 error path now answers with a raw 500 (_try_write_server_error!, same as sibling branches) when the handler fails with response_started set but nothing committed. Once the head is on the wire (e.g. chunked mode writes it at startwrite), behavior is unchanged — the control test asserts no second head/spurious 500 is emitted.

scenario before after
throw after startwrite, FIXED (deferred head) connection drop, client ParseError: unexpected EOF 500 Internal Server Error
throw after startwrite, chunked (head committed) connection close unchanged

Tests

3 new testsets in http_server_http1_tests.jl (codeunits round-trip, deferred-startwrite → 500, committed-head control). Locally: h1 189/189, client 472/472, core 162/162.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 87.03%. Comparing base (94c71fe) to head (80a72a8).

Files with missing lines Patch % Lines
src/http_transport.jl 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1305      +/-   ##
==========================================
+ Coverage   87.02%   87.03%   +0.01%     
==========================================
  Files          28       28              
  Lines       11017    11026       +9     
==========================================
+ Hits         9587     9597      +10     
+ Misses       1430     1429       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

quinnj and others added 2 commits June 12, 2026 12:34
#1302, #1303)

#1302: Base defines write(::IO, ::Base.CodeUnits), which is ambiguous with
the Stream/_RequestDeadlineWriteIO StridedVector{UInt8} write methods
(CodeUnits <: DenseVector{UInt8}). Add disambiguating CodeUnits methods so
write(stream, codeunits(s)) dispatches to the bytes path on both the client
and server stream sides.

#1303: h1 FIXED mode defers the response head at startwrite while flipping
response_started, so a handler throw after startwrite skipped the 500
fallback (gated on !response_started) and silently dropped the connection;
clients saw "unexpected EOF". Track head_committed -- set only where head
bytes actually reach the transport (h1 and h2 paths) -- and answer with a
raw 500 when the handler fails with nothing committed. Once the head is on
the wire (e.g. chunked), behavior is unchanged: no second head is emitted.

Tests: codeunits round-trip through a real handler; deferred-startwrite
throw now yields 500; chunked control asserts no spurious 500 after a
committed head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The raw HTTP/1 probe helper fails with "timed out waiting for first
response byte" when the known Windows IOCP wake strand
(JuliaServices/Reseau.jl#107) leaves the probe's parked reader unwoken —
the same canary test has now struck four unrelated PRs. Until the Reseau
fix lands, re-dial fresh and retry (3 attempts, Windows only; other
platforms stay strict) with a loud warning so the strand remains visible
in logs. Safe for all current callers: none assert on server-side
invocation counts. The first-byte timeout is now a typed exception so the
retry can unwrap it through the nested watchdog wrappers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@quinnj quinnj force-pushed the jq-stream-error-fixes branch from 9d6cc2f to a718a79 Compare June 12, 2026 19:46
…te IO

Covers the Stream{true} and _RequestDeadlineWriteIO CodeUnits
disambiguation methods added for #1302.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@quinnj quinnj merged commit 8bd18fe into master Jun 12, 2026
8 checks passed
@quinnj quinnj deleted the jq-stream-error-fixes branch June 12, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant