Skip to content

fix(http): percent-decode h1 request-target paths to h2/h3 parity; drop StaticFiles compensation [L01.01.11.43] - #933

Merged
dotnetcadet merged 2 commits into
mainfrom
feature/L01.01.11.43-h1-percent-decode
Jul 20, 2026
Merged

fix(http): percent-decode h1 request-target paths to h2/h3 parity; drop StaticFiles compensation [L01.01.11.43]#933
dotnetcadet merged 2 commits into
mainfrom
feature/L01.01.11.43-h1-percent-decode

Conversation

@dotnetcadet

Copy link
Copy Markdown
Contributor

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 :path via HttpPath.FromUriComponent before middleware sees it; the h1 reader surfaced raw request-target text — so %2e%2e was a literal segment on h1 but .. on h2/h3.

  • h1 fix in Http1MessageReader: origin-form request-targets now run the already-split path through the identical HttpPath.FromUriComponent decode (%2F stays 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.Parse on 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.
  • StaticFiles compensation removed same-change (the Http11-gated Uri.UnescapeDataString that would now double-decode); its pinned tests re-pointed, DESIGN parity section rewritten, plus an anti-double-decode guard test.
  • The issue's %20 premise inverted — worth owner attention: decoded space/control/#/?/NUL are illegal HttpPath characters repo-wide, so parity means my%20file.txt is 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 an HttpPath character-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 ParseQuery code) · Web.StaticFiles 85 (real-h1 traversal e2e + positive %24$ round-trip) · Web.Routing 250 · Http core 1231 — all passing.

Follow-up candidates (not filed)

  1. h2/h3 uncaught decode throwsHttp2ConnectionContext/h3 dispatch don't catch ParseQuery decode failures, so a malformed :path throws uncaught (h1 now handles it gracefully); should become a graceful stream reset. Robustness/DoS-adjacent.
  2. HttpPath character-set widening if space-named resources should be servable.
  3. HttpFormReader's decode carries a pre-existing accuracy TODO.

Closes #895

🤖 Generated with Claude Code

dotnetcadet and others added 2 commits July 20, 2026 10:48
…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>
Base automatically changed from feature/L01.01.11.44-h3-control-stream to main July 20, 2026 18:21
@dotnetcadet
dotnetcadet merged commit 6ff9ccf into main Jul 20, 2026
114 checks passed
@dotnetcadet
dotnetcadet deleted the feature/L01.01.11.43-h1-percent-decode branch July 20, 2026 18:23
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.43] HTTP/1.1 request-target percent-decode parity with h2/h3

1 participant