fix(http): percent-decode h1 request-target paths to h2/h3 parity; drop StaticFiles compensation [L01.01.11.43] - #933
Merged
Conversation
…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>
…op StaticFiles compensation [L01.01.11.43] Http1MessageReader now surfaces IHttpRequest.Path from the origin-form request-target through the same HttpPath.FromUriComponent decode HTTP/2 (Http2Stream) and HTTP/3 (Http3HeaderCodec) run over the :path pseudo-header, so identical wire bytes yield an identical decoded path on every transport: %2e%2e decodes to "..", %2F stays encoded (never a separator), ordinary octets decode, and invalid/overlong escapes are left intact per UrlDecoder. A decoded octet that is not a legal path character (space, control, ?/#, NUL) makes the target malformed and is surfaced as the reader's existing wire-level failure. Only origin-form is decoded; absolute/authority/asterisk keep their handling. The query is unchanged — already split before the decode and parsed by HttpQuery.Parse identically on all transports. Removes the now-double-decoding version-gated compensation in Web.StaticFiles.StaticFilesMiddleware (Http11 + Uri.UnescapeDataString) in the same change and re-points its tests: the encoded-traversal/name decode is pinned at the transport (new Http.Connections parity suite + the E2E traversal/name round-trips), and a middleware test guards against re-decoding a handed path. Tests: Http.Connections 434, Web.StaticFiles 85, Web.Routing 250, Http core 1231 — all green. Updates both touched DESIGN.md files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Batch 5, stack 2/4 (base:
feature/L01.01.11.44-h3-control-stream, PR #932).Closes the transport divergence #777 discovered: h2/h3 percent-decode
:pathviaHttpPath.FromUriComponentbefore middleware sees it; the h1 reader surfaced raw request-target text — so%2e%2ewas a literal segment on h1 but..on h2/h3.Http1MessageReader: origin-form request-targets now run the already-split path through the identicalHttpPath.FromUriComponentdecode (%2Fstays encoded — the routing{**}invariant;%2e%2e→..; invalid/overlong sequences left intact). Absolute/authority/asterisk forms keep current handling; query handling was already at parity (HttpQuery.Parseon all three). Decode failures surface as the transport's existing malformed-request-target failure — narrowly-scoped catch, no behavior change to limits/dispatch-at-head.Http11-gatedUri.UnescapeDataStringthat would now double-decode); its pinned tests re-pointed, DESIGN parity section rewritten, plus an anti-double-decode guard test.%20premise inverted — worth owner attention: decoded space/control/#/?/NUL are illegalHttpPathcharacters repo-wide, so parity meansmy%20file.txtis now uniformly rejected on every transport (h1 previously served it for static files only via the compensation; h2/h3 have always thrown). Making space-named resources reachable would be anHttpPathcharacter-set decision, not a transport one — recorded as a follow-up candidate.Tests
Http.Connections 434 (12 new: h1+h2 parity matrix Theory over the four decode categories, query-contrast pin, h1 rejection Theory — h3 guaranteed by byte-identical
ParseQuerycode) · Web.StaticFiles 85 (real-h1 traversal e2e + positive%24→$round-trip) · Web.Routing 250 · Http core 1231 — all passing.Follow-up candidates (not filed)
Http2ConnectionContext/h3 dispatch don't catchParseQuerydecode failures, so a malformed:paththrows uncaught (h1 now handles it gracefully); should become a graceful stream reset. Robustness/DoS-adjacent.HttpPathcharacter-set widening if space-named resources should be servable.HttpFormReader's decode carries a pre-existing accuracy TODO.Closes #895
🤖 Generated with Claude Code