refactor(web): rebase RouteHostConstraint host:port splitting onto HttpHost [L03.01.02.01.09] - #934
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>
…ared HttpHost primitive [L03.01.02.01.09] Host filtering (#781) added a structural host[:port] split to HttpHost (TryGetComponents) that deliberately mirrored RouteHostConstraint's own private TrySplitHostAndPort/TryParsePort (#788). The two copies could drift, so route host SELECTION and host allowlist VALIDATION might disagree on what a wire value means. Extract the structural split as an internal HttpHost.TrySplitHostPort and widen TryParsePort to internal (granted to Web.Routing via InternalsVisibleTo). TryGetComponents now layers its stricter port validation on top of that shared split, byte-for-byte unchanged. RouteHostConstraint.TryParse/IsMatch delegate to the shared helpers and the duplicated private copies are deleted. Direct delegation to TryGetComponents was impossible without changing behavior: it fuses port validation into the split and returns false (no host) for a present-but-invalid port, which would break the documented #788 leniency where a port-unconstrained route tolerates junk port text in the request host and matches on the host part alone. The shared *structural* split preserves that leniency exactly; new RouteHostConstraintTests cases pin both the leniency and its structural boundary. 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 3/4 (base:
feature/L01.01.11.43-h1-percent-decode, PR #933).The #781 host-filtering work gave core
HttpHosta structural host[:port] split deliberately mirroringRouteHostConstraint's; the constraint (from #788) still carried its own private copy. This rebases the constraint onto the shared primitive so the SELECT (routing) and VALIDATE (filtering) paths cannot drift.TryGetComponents' fused parse refuses the whole host. Direct delegation would have broken the leniency.internal static HttpHost.TrySplitHostPort(byte-identical to the constraint's old code), widenedTryParsePortto internal, and rebuiltTryGetComponentsas a layer over them (verified against all 21 existingHttpHostTestscases).RouteHostConstraintnow delegates; its duplicated private split/parse is deleted (−84 lines). Shared helpers stay internal with a rationale-commentedInternalsVisibleTo("Assimalign.Cohesion.Web.Routing")— no public surface growth, no reference-graph change (the sanctioned "shared span helper in core Http" shape from the issue).Tests
Web.Routing 263 (250 pre-existing UNCHANGED + 13 new) · core Http 1231 · Web.HostFiltering 14 · Web.HttpsPolicy 32 — all passing. No public-surface change to either type.
Closes #890
🤖 Generated with Claude Code