Skip to content

fix(http): end the h3 request stream at response completion [L01.01.11.44] - #932

Merged
dotnetcadet merged 1 commit into
mainfrom
feature/L01.01.11.44-h3-control-stream
Jul 20, 2026
Merged

fix(http): end the h3 request stream at response completion [L01.01.11.44]#932
dotnetcadet merged 1 commit into
mainfrom
feature/L01.01.11.44-h3-control-stream

Conversation

@dotnetcadet

Copy link
Copy Markdown
Contributor

Summary

Batch 5, stack 1/4 (base: main).

Root-caused and fixed the defect behind every failed HTTP/3 round-trip. The control stream was innocent — the issue title's hypothesis did not survive contact with the evidence:

  • Root cause: an HTTP/3 response body is delimited by the request stream's end (RFC 9114 §4.1), not Content-Length — and the h3 send path never ended the stream's write side. SendAsync wrote HEADERS/DATA, flushed, and returned; the stream was only completed at whole-connection teardown. A real .NET client sits in ReadResponseContentAsync waiting for the FIN, and when teardown finally arrives it surfaces as H3_CLOSED_CRITICAL_STREAM (0x104) — the critical streams closing was the symptom the client happened to report, not the cause. Proven by a decisive experiment: adding the FIN flips the failing example to a clean 200 + body.
  • Fix (minimal, Lane A internal): emit the graceful QUIC FIN (Output.Complete() — the IConnection half-close contract) once the response is fully flushed, in both send paths: the buffered path (CompleteResponseStreamWrites at the end of SendAsync) and the streaming path (Http3ResponseBodyStream.CompleteFramedAsync now flushes then completes). Best-effort with narrowly-scoped catches (disposed / wire-level teardown races are benign — the bytes are already delivered).
  • The two associated observations from [L03.01.01.01.06] Add HTTP/3 (QUIC) registration surface to the Web server builder #767 bring-up: the bodyless-200 hang was the SAME root cause (Content-Length: 0 + no FIN = client drains forever) — fixed and pinned. The body-position concern was NOT a live defect (ReadBodyAsync reads position-independently via ToArray()) — locked in with a regression test, no code change.
  • [L03.01.01.01.06] Add HTTP/3 (QUIC) registration surface to the Web server builder #767's e2e strengthened: WebHttp3HostingIntegrationTests best-effort response handling flipped to hard client-observed status + body assertions; Web.Hosting DESIGN.md's "blocked defect" posture rewritten as verified. The Http.Connections h3 example now completes a full round-trip.

Tests

Http.Connections 422 (4 new platform-guarded real-QUIC round-trip tests + a new Http3LoopbackServer harness; QUIC e2e genuinely ran on this machine, not skipped) · Web.Hosting 58 — all passing.

Follow-up candidate (not filed)

Request-stream wrapper lifecycle is connection-scoped (QuicMultiplexedConnection.\_streams untracks only at connection teardown) — unbounded wrapper accumulation on long-lived h3 connections; pre-existing, neither introduced nor worsened here.

Closes #928

🤖 Generated with Claude Code

…ient sees the response terminate [L01.01.11.44]

An HTTP/3 response body is delimited by the request stream's end (RFC 9114
§4.1), not by Content-Length. The h3 send path wrote HEADERS/DATA, flushed,
and returned without ending the request stream's write side, so a real .NET
HTTP/3 client stayed in ReadResponseContentAsync waiting for a FIN that only
arrived at connection teardown — at which point the control/QPACK critical
streams closing surfaced at the client as H3_CLOSED_CRITICAL_STREAM (0x104).

The buffered SendAsync and the streaming sink's CompleteFramedAsync now end
the request stream's write side (graceful QUIC FIN via the IConnection
half-close contract, Output.Complete()) once the response is fully flushed.
Best-effort: a teardown race that disposed the stream is swallowed.

Both #928 send observations resolve here: a bodyless 200 completes the
client's zero-length drain via the FIN, and the buffered-body content length
already matched the DATA written (ReadBodyAsync reads the whole buffer,
position-independent). Adds platform-guarded real-QUIC round-trip regression
tests (full round-trip, bodyless 200, end-position body, sequential requests
pinning critical-stream lifetime) and flips the Web.Hosting UseHttp3 e2e from
best-effort to a hard client-observed status+body assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dotnetcadet
dotnetcadet merged commit 4a13265 into main Jul 20, 2026
114 checks passed
@dotnetcadet
dotnetcadet deleted the feature/L01.01.11.44-h3-control-stream branch July 20, 2026 18:21
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.

[L01.01.11.44] Fix HTTP/3 server control-stream teardown: response round-trip fails with H3_CLOSED_CRITICAL_STREAM

1 participant