Skip to content

[L03.01.02.01.08] Content-serialization registry + OnError hook — Web.Serialization + Web.ErrorHandling - #893

Merged
dotnetcadet merged 3 commits into
mainfrom
feature/L03.01.02.01.08-serialization-onerror
Jul 16, 2026
Merged

[L03.01.02.01.08] Content-serialization registry + OnError hook — Web.Serialization + Web.ErrorHandling#893
dotnetcadet merged 3 commits into
mainfrom
feature/L03.01.02.01.08-serialization-onerror

Conversation

@dotnetcadet

Copy link
Copy Markdown
Contributor

Summary

Implements the re-scoped #864 (2026-07-10 owner decision): the two pipeline seams that succeeded the withdrawn IResult abstraction, delivered as two new Web feature libraries. The Web area stays middleware-first — no result carriers, no Results/TypedResults factories, handlers write responses imperatively.

  1. Assimalign.Cohesion.Web.Serialization — the content-serialization registry. Media-type-keyed request-deserialization / response-serialization halves (IHttpContentReader / IHttpContentWriter, deliberately not one god ISerializer), exposed per exchange as IHttpContentSerializationFeature. Builder-time registration per the issue's shape — builder.AddJsonSerialization(AppJsonContext.Default) registers the built-in JSON pair over a source-generated IJsonTypeInfoResolver — and typed call sites request.ReadContentAsync<T>() / response.WriteContentAsync(value) with zero reflection under NativeAOT (JsonTypeInfo-based STJ entry points only, MakeReadOnly() frozen options, no reflection fallback: uncovered types fault instead).
  2. Assimalign.Cohesion.Web.ErrorHandling — the OnError hook. builder.AddErrorHandling().OnError(...) registers IHttpErrorHandlers consulted in registration order; the first to return true owns the fault; when all pass, the terminal default renders the RFC 9457 Web.ProblemDetails payload via WriteProblemDetailsAsync (500, application/problem+json, about:blank, no exception detail leaked). Exposed per exchange as IHttpErrorHandlingFeature.HandleAsync(context, exception) — the exact seam the [L03.01.01.19] Pipeline exception boundary, status-code pages, and 404 terminal over IResult (supersedes #776) #881 exception boundary invokes.

Design decisions recorded in docs/DESIGN.md (per the acceptance criteria)

  • Faults vs outcomes — the line both packages hold: expected protocol outcomes (401 challenge, 404, 415) stay each feature's normal imperative response path and are never thrown; the hook is for faults only (key ring unavailable, missing serialization contract). The registry's feature surface is non-throwing (GetReader/GetWriternull) so outcome-producing layers branch to 415/406 without exceptions; only the call-site conveniences throw (HttpContentSerializationException, a fault for the hook).
  • Package homing under the hosting-isolation rule — the issue's open question, decided by the default handler: it renders Web.ProblemDetails, the area root must not reference feature packages, so the hook lives in a feature package (Web.ErrorHandlingHttp, Web, Web.ProblemDetails), not an area-root seam. Web.Hosting references neither package (COHRES001/002 clean); delivery is via the App.Web shared framework; the server's [L03.01.01.01.04] Rewrite WebApplicationServer: per-connection dispatch, error isolation, disposal, graceful stop #762 last-resort isolation stays infrastructure-level and never invokes the hook (three-layer model recorded in DESIGN.md).
  • Non-generic contracts, generic extensions — registry interfaces take (object? value, Type type) (no generic virtual dispatch under AOT; heterogeneous storage); ReadContentAsync<T>/WriteContentAsync<T> forward typeof(T).
  • Matching is [L01.01.11.38] Add HttpMediaType value object and Accept/quality-value negotiation primitives #771 semantics, nothing inventedHttpMediaType.Includes + Specificity ranking + registration-order tie-break, so [L03.01.02.01.02] Implement result writers and content negotiation primitives needed by common endpoint scenarios #149's negotiation and the registry can never disagree. application/*+json suffix ranges are deliberately not a match rule (recorded; HttpMediaType.Suffix is there for [L03.01.02.01.02] Implement result writers and content negotiation primitives needed by common endpoint scenarios #149).
  • AddErrorHandling().OnError(...) chain, not builder.OnError(...) — a root-level repeatable-looking verb would silently replace the name-keyed feature and drop earlier handlers; the auth-idiom chained builder makes repetition safe. Composition/ordering question answered structurally: registration order = consultation order, first-true wins, terminal default is not a registration (overriding = registering a handler that always handles).
  • ReadContentAsync/WriteContentAsync naming — the issue's response.WriteAsync(value) sketch was kept ergonomically but renamed: an unconstrained WriteAsync<T> would silently change overload binding for string the day a raw-text WriteAsync helper appears.

Deliberately not in this PR (the fan-out this seam exists for)

Wiring (new-Web-project checklist)

  • frameworks/Assimalign.Cohesion.App.props — both assemblies added to the App.Web ItemGroup (no new outside-area transitive deps needed); validated via dotnet pack of App.Web.Runtime.
  • .github/workflows/resource-web.yml — both projects added to the CI matrix (COHRES guard executes per project).
  • resources/Web/Assimalign.Cohesion.Web.slnx + root Assimalign.Cohesion.slnx — src/tests/docs entries.
  • resources/Web/README.md — project-map rows.
  • docs/OVERVIEW.md + docs/DESIGN.md per package.
  • Note (Batch-4a collision): sibling Wave-4a sessions touch the same App.props/CI-matrix lines — trivial rebase expected, as planned.

Tests

39 new tests (27 serialization, 12 error handling), all green: registry matching semantics (specificity, ties, parameterized types, null on no match), JSON round-trips over the source-generated TestJsonContext (web-default camelCase, contract faults, format-native JsonException propagation), chain semantics (order, first-wins, handler-fault propagation, default rendering, no-internals-leak), plus full-pipeline tests over the merged #793 WebApplicationTestFactory: typed body round-trip end-to-end, middleware branching to a 415 outcome via the non-throwing surface, an escaped fault rendering as problem+json 500 through an inline boundary, and a registered OnError handler owning a 503 response.

Acceptance-criteria note

The issue gated implementation on an owner design review; the owner's session brief for this item directed full implementation → PR. The docs/DESIGN.md files are the recorded design (registry shape + AOT story, hook contract + default behavior, faults-vs-outcomes, homing), and this PR review is that owner gate — if any recorded decision should go the other way, the seams are small enough to pivot before merge.

Closes #864

🤖 Generated with Claude Code

dotnetcadet and others added 3 commits July 11, 2026 14:07
…zation, Web.ErrorHandling) [L03.01.02.01.08]

Implements the re-scoped #864: two new Web feature libraries under the
middleware-first direction (no IResult, no result carriers).

- Web.Serialization: media-type-keyed registry with distinct reader/writer
  halves, AddJsonSerialization over a source-generated IJsonTypeInfoResolver
  (JsonTypeInfo-only STJ entry points, zero reflection under NativeAOT), and
  ReadContentAsync/WriteContentAsync call-site extensions.
- Web.ErrorHandling: AddErrorHandling().OnError(...) fault-handler chain
  (registration order, first-true owns) with a terminal default rendering the
  Web.ProblemDetails payload (500, problem+json, no exception detail).

Both are feature packages on the area root's seams (COHRES-clean; Web.Hosting
references neither), delivered via the App.Web shared framework, and wired
into the framework manifest, CI matrix, both slnx files, and the area README.
DESIGN.md files record the registry shape/AOT story, hook contract/default
behavior, faults-vs-outcomes rule, and homing rationale for #881/#149/#796.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…08-serialization-onerror

# Conflicts:
#	resources/Web/README.md
…08-serialization-onerror

# Conflicts:
#	.github/workflows/resource-web.yml
#	Assimalign.Cohesion.slnx
#	frameworks/Assimalign.Cohesion.App.props
#	resources/Web/Assimalign.Cohesion.Web.slnx
@dotnetcadet
dotnetcadet merged commit 94ad362 into main Jul 16, 2026
173 checks passed
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…al via OnError hook [L03.01.01.19] (#920)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…1.02.01.02] (#921)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…on [L03.01.01.08] (#922)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…port-Security header key [L03.01.01.09] (#923)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.HttpsPolicy HTTPS redirection + HSTS; fix Strict-Transport-Security header key [L03.01.01.09]

Add resources/Web/Assimalign.Cohesion.Web.HttpsPolicy, one lean feature package
pairing both HTTPS-posture concerns as extension(IWebApplicationPipelineBuilder)
verbs (no DI/config/logging; refs Web + Http only, never Web.Hosting):

- UseHttpsRedirection: insecure request -> bodyless method-preserving redirect
  (307 default, 308 configurable); Location rebuilt from the request (https
  scheme, host with its port swapped to the configured HTTPS port — 443 omitted,
  IPv6 re-bracketed; path verbatim; query reconstructed from the parsed
  collection). HttpsPort is explicit (Web.Hosting isolation forbids deriving it).
- UseHsts: RFC 6797 Strict-Transport-Security composed once at builder time
  (max-age default 365d, includeSubDomains, preload), emitted on secure responses
  only (RFC 6797 7.2), loopback (localhost/127.0.0.1/[::1]) excluded by default
  via the core HttpHostMatcher. Applied post-next (outside the #881 boundary) so
  it survives the boundary's header reset on a faulted response; skipped on a
  committed head via IHttpHeaderCollection.IsReadOnly.

Connection security is read from the transport-derived typed IHttpRequest.Scheme
(#763), not scheme-string sniffing.

Also fix HttpHeaderKey.StrictTransportSecurity value from the misspelled
"Strict-Transports-Security" to "Strict-Transport-Security" (RFC 6797), with
round-trip tests through IHttpHeaderCollection (member name unchanged).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, README map,
OVERVIEW/DESIGN docs. Tests: 32 new (redirection + HSTS) via unit-level context
and pipeline-builder doubles; 3 new core Http header-key tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): note TLS-terminating-proxy behavior for HttpsPolicy scheme detection

Review follow-up: Request.Scheme is the immediate hop; Web.ForwardedHeaders
deliberately never mutates it, so document the plaintext-hop redirect-loop
hazard and the IHttpForwardedFeature follow-up rather than leaving it implicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…cies [L03.01.01.12] (#924)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.HttpsPolicy HTTPS redirection + HSTS; fix Strict-Transport-Security header key [L03.01.01.09]

Add resources/Web/Assimalign.Cohesion.Web.HttpsPolicy, one lean feature package
pairing both HTTPS-posture concerns as extension(IWebApplicationPipelineBuilder)
verbs (no DI/config/logging; refs Web + Http only, never Web.Hosting):

- UseHttpsRedirection: insecure request -> bodyless method-preserving redirect
  (307 default, 308 configurable); Location rebuilt from the request (https
  scheme, host with its port swapped to the configured HTTPS port — 443 omitted,
  IPv6 re-bracketed; path verbatim; query reconstructed from the parsed
  collection). HttpsPort is explicit (Web.Hosting isolation forbids deriving it).
- UseHsts: RFC 6797 Strict-Transport-Security composed once at builder time
  (max-age default 365d, includeSubDomains, preload), emitted on secure responses
  only (RFC 6797 7.2), loopback (localhost/127.0.0.1/[::1]) excluded by default
  via the core HttpHostMatcher. Applied post-next (outside the #881 boundary) so
  it survives the boundary's header reset on a faulted response; skipped on a
  committed head via IHttpHeaderCollection.IsReadOnly.

Connection security is read from the transport-derived typed IHttpRequest.Scheme
(#763), not scheme-string sniffing.

Also fix HttpHeaderKey.StrictTransportSecurity value from the misspelled
"Strict-Transports-Security" to "Strict-Transport-Security" (RFC 6797), with
round-trip tests through IHttpHeaderCollection (member name unchanged).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, README map,
OVERVIEW/DESIGN docs. Tests: 32 new (redirection + HSTS) via unit-level context
and pipeline-builder doubles; 3 new core Http header-key tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): note TLS-terminating-proxy behavior for HttpsPolicy scheme detection

Review follow-up: Request.Scheme is the immediate hop; Web.ForwardedHeaders
deliberately never mutates it, so document the plaintext-hop redirect-loop
hazard and the IHttpForwardedFeature follow-up rather than leaving it implicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.RateLimiting inbound rate-limiting middleware and policies [L03.01.01.12]

New resources/Web feature package adapting the BCL System.Threading.RateLimiting
engine to the Web pipeline — supplying the middleware, policy model, partition-key
surface, and rejection response, never a limiter algorithm (#783).

- UseRateLimiting composes a global limiter (applied to every request, acquired
  up-front with full queueing) plus named policies attached per-endpoint as the
  sealed RateLimitingMetadata carrier in the routing metadata bag.
- Per-endpoint gating mirrors the Web.RequestTimeouts seam: a decorated context
  reads the matched endpoint's metadata at the router's route-match publication.
  Since the router matches and dispatches in one middleware, the sync gate uses
  AttemptAcquire and raises an internal signal the middleware catches to answer
  before the handler runs. Global + endpoint are additive (both must grant a lease).
- Partitioned via PartitionedRateLimiter<IHttpContext> with AOT-safe selectors:
  RateLimitPartitionKeys.ClientAddress composes the Http.Forwarded effective client
  (trust-gated; BCP 38 caution documented), plus Header and typed selectors.
- Rejection: 429 (configurable) + Retry-After from lease metadata, OnRejected hook
  (bodyless default composes with #881 status-code pages), OnDecision observation
  hook in place of a hosting-layer OTel seam (re-scoped, follow-up recorded).
- IRateLimitingFeature exposes the decision downstream. Limiters live for the
  application lifetime; per-request leases released on completion (disposal posture
  documented).

Wiring: App.Web manifest, both slnx, resource-web.yml matrix, README project map,
OVERVIEW/DESIGN docs. System.Threading.RateLimiting is not added to the framework
manifest — it carries only Cohesion assemblies, matching the existing
Resilience.RateLimiting precedent in base App. 20 new tests (15 middleware unit +
5 e2e over WebApplicationTestFactory), all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): document RateLimiting signal ordering constraint vs the exception boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
….01.14] (#925)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.HttpsPolicy HTTPS redirection + HSTS; fix Strict-Transport-Security header key [L03.01.01.09]

Add resources/Web/Assimalign.Cohesion.Web.HttpsPolicy, one lean feature package
pairing both HTTPS-posture concerns as extension(IWebApplicationPipelineBuilder)
verbs (no DI/config/logging; refs Web + Http only, never Web.Hosting):

- UseHttpsRedirection: insecure request -> bodyless method-preserving redirect
  (307 default, 308 configurable); Location rebuilt from the request (https
  scheme, host with its port swapped to the configured HTTPS port — 443 omitted,
  IPv6 re-bracketed; path verbatim; query reconstructed from the parsed
  collection). HttpsPort is explicit (Web.Hosting isolation forbids deriving it).
- UseHsts: RFC 6797 Strict-Transport-Security composed once at builder time
  (max-age default 365d, includeSubDomains, preload), emitted on secure responses
  only (RFC 6797 7.2), loopback (localhost/127.0.0.1/[::1]) excluded by default
  via the core HttpHostMatcher. Applied post-next (outside the #881 boundary) so
  it survives the boundary's header reset on a faulted response; skipped on a
  committed head via IHttpHeaderCollection.IsReadOnly.

Connection security is read from the transport-derived typed IHttpRequest.Scheme
(#763), not scheme-string sniffing.

Also fix HttpHeaderKey.StrictTransportSecurity value from the misspelled
"Strict-Transports-Security" to "Strict-Transport-Security" (RFC 6797), with
round-trip tests through IHttpHeaderCollection (member name unchanged).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, README map,
OVERVIEW/DESIGN docs. Tests: 32 new (redirection + HSTS) via unit-level context
and pipeline-builder doubles; 3 new core Http header-key tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): note TLS-terminating-proxy behavior for HttpsPolicy scheme detection

Review follow-up: Request.Scheme is the immediate hop; Web.ForwardedHeaders
deliberately never mutates it, so document the plaintext-hop redirect-loop
hazard and the IHttpForwardedFeature follow-up rather than leaving it implicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.RateLimiting inbound rate-limiting middleware and policies [L03.01.01.12]

New resources/Web feature package adapting the BCL System.Threading.RateLimiting
engine to the Web pipeline — supplying the middleware, policy model, partition-key
surface, and rejection response, never a limiter algorithm (#783).

- UseRateLimiting composes a global limiter (applied to every request, acquired
  up-front with full queueing) plus named policies attached per-endpoint as the
  sealed RateLimitingMetadata carrier in the routing metadata bag.
- Per-endpoint gating mirrors the Web.RequestTimeouts seam: a decorated context
  reads the matched endpoint's metadata at the router's route-match publication.
  Since the router matches and dispatches in one middleware, the sync gate uses
  AttemptAcquire and raises an internal signal the middleware catches to answer
  before the handler runs. Global + endpoint are additive (both must grant a lease).
- Partitioned via PartitionedRateLimiter<IHttpContext> with AOT-safe selectors:
  RateLimitPartitionKeys.ClientAddress composes the Http.Forwarded effective client
  (trust-gated; BCP 38 caution documented), plus Header and typed selectors.
- Rejection: 429 (configurable) + Retry-After from lease metadata, OnRejected hook
  (bodyless default composes with #881 status-code pages), OnDecision observation
  hook in place of a hosting-layer OTel seam (re-scoped, follow-up recorded).
- IRateLimitingFeature exposes the decision downstream. Limiters live for the
  application lifetime; per-request leases released on completion (disposal posture
  documented).

Wiring: App.Web manifest, both slnx, resource-web.yml matrix, README project map,
OVERVIEW/DESIGN docs. System.Threading.RateLimiting is not added to the framework
manifest — it carries only Cohesion assemblies, matching the existing
Resilience.RateLimiting precedent in base App. 20 new tests (15 middleware unit +
5 e2e over WebApplicationTestFactory), all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): document RateLimiting signal ordering constraint vs the exception boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): async session-store seam + Web.Sessions middleware [L03.01.01.14]

Http.Sessions: add the async IHttpSessionStore seam (get/set/renew/remove
keyed by session id, opaque byte[] payload, idle-timeout metadata) with the
in-memory dictionary moved behind it as the default InMemoryHttpSessionStore.
Payload framing is a version-prefixed, length-prefixed binary codec
(HttpSessionSerializer) — no reflection — so any store round-trips identical
bytes. The concurrency contract is last-commit-wins (documented on the
interface + DESIGN.md, pinned by tests); sliding expiration is renew-on-access.
An internal store-backed session composes the store with the frame codec. Every
existing IHttpSession caller and test is unchanged.

Web.Sessions: fill the empty scaffold with the per-request middleware.
UseSessions installs a lazy, store-backed session on the exchange (no store I/O
or cookie until first access), establishes a hardened session-id cookie
(HttpOnly, SameSite=Lax, Secure on HTTPS, session-scoped) via the Http.Cookies
model synchronously at id-mint time (before the head commits), and commits
after next (persist-if-modified, else slide). Session ids are 128-bit
crypto-random base64url; RegenerateSessionIdAsync is the post-auth fixation
defense (new id, old id removed, cookie replaced). Distributed backends are
deferred to IHttpSessionStore adapters.

Wiring: Web.Sessions docs (OVERVIEW/DESIGN), Http.Sessions DESIGN.md scope
update, README project-map row, slnx docs entries. Manifest + CI matrix already
carried the scaffold; App.Web.Runtime pack validated with both assemblies.

Tests: Http.Sessions 47 (23 existing regression + 24 new), Web.Sessions 15
(unit middleware + e2e round-trip over WebApplicationTestFactory). Stack
regressions green (ErrorHandling/Serialization/Compression/HttpsPolicy/
RateLimiting/Hosting).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): never persist an orphaned new session whose cookie could not be delivered

Review follow-ups on the #785 branch: a session first accessed after the response
head commits mints an id the client can never present again — skip the store
persist instead of littering the store until idle-timeout reaping (externally
supplied sessions keep their caller-owned commit lifecycle); pin with a
RecordingSessionStore fixture and an existing-cookie-after-head counterpart
test; true up the stale Http.Sessions csproj description (Items bag -> typed
feature collection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.HttpsPolicy HTTPS redirection + HSTS; fix Strict-Transport-Security header key [L03.01.01.09]

Add resources/Web/Assimalign.Cohesion.Web.HttpsPolicy, one lean feature package
pairing both HTTPS-posture concerns as extension(IWebApplicationPipelineBuilder)
verbs (no DI/config/logging; refs Web + Http only, never Web.Hosting):

- UseHttpsRedirection: insecure request -> bodyless method-preserving redirect
  (307 default, 308 configurable); Location rebuilt from the request (https
  scheme, host with its port swapped to the configured HTTPS port — 443 omitted,
  IPv6 re-bracketed; path verbatim; query reconstructed from the parsed
  collection). HttpsPort is explicit (Web.Hosting isolation forbids deriving it).
- UseHsts: RFC 6797 Strict-Transport-Security composed once at builder time
  (max-age default 365d, includeSubDomains, preload), emitted on secure responses
  only (RFC 6797 7.2), loopback (localhost/127.0.0.1/[::1]) excluded by default
  via the core HttpHostMatcher. Applied post-next (outside the #881 boundary) so
  it survives the boundary's header reset on a faulted response; skipped on a
  committed head via IHttpHeaderCollection.IsReadOnly.

Connection security is read from the transport-derived typed IHttpRequest.Scheme
(#763), not scheme-string sniffing.

Also fix HttpHeaderKey.StrictTransportSecurity value from the misspelled
"Strict-Transports-Security" to "Strict-Transport-Security" (RFC 6797), with
round-trip tests through IHttpHeaderCollection (member name unchanged).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, README map,
OVERVIEW/DESIGN docs. Tests: 32 new (redirection + HSTS) via unit-level context
and pipeline-builder doubles; 3 new core Http header-key tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): note TLS-terminating-proxy behavior for HttpsPolicy scheme detection

Review follow-up: Request.Scheme is the immediate hop; Web.ForwardedHeaders
deliberately never mutates it, so document the plaintext-hop redirect-loop
hazard and the IHttpForwardedFeature follow-up rather than leaving it implicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.RateLimiting inbound rate-limiting middleware and policies [L03.01.01.12]

New resources/Web feature package adapting the BCL System.Threading.RateLimiting
engine to the Web pipeline — supplying the middleware, policy model, partition-key
surface, and rejection response, never a limiter algorithm (#783).

- UseRateLimiting composes a global limiter (applied to every request, acquired
  up-front with full queueing) plus named policies attached per-endpoint as the
  sealed RateLimitingMetadata carrier in the routing metadata bag.
- Per-endpoint gating mirrors the Web.RequestTimeouts seam: a decorated context
  reads the matched endpoint's metadata at the router's route-match publication.
  Since the router matches and dispatches in one middleware, the sync gate uses
  AttemptAcquire and raises an internal signal the middleware catches to answer
  before the handler runs. Global + endpoint are additive (both must grant a lease).
- Partitioned via PartitionedRateLimiter<IHttpContext> with AOT-safe selectors:
  RateLimitPartitionKeys.ClientAddress composes the Http.Forwarded effective client
  (trust-gated; BCP 38 caution documented), plus Header and typed selectors.
- Rejection: 429 (configurable) + Retry-After from lease metadata, OnRejected hook
  (bodyless default composes with #881 status-code pages), OnDecision observation
  hook in place of a hosting-layer OTel seam (re-scoped, follow-up recorded).
- IRateLimitingFeature exposes the decision downstream. Limiters live for the
  application lifetime; per-request leases released on completion (disposal posture
  documented).

Wiring: App.Web manifest, both slnx, resource-web.yml matrix, README project map,
OVERVIEW/DESIGN docs. System.Threading.RateLimiting is not added to the framework
manifest — it carries only Cohesion assemblies, matching the existing
Resilience.RateLimiting precedent in base App. 20 new tests (15 middleware unit +
5 e2e over WebApplicationTestFactory), all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): document RateLimiting signal ordering constraint vs the exception boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): async session-store seam + Web.Sessions middleware [L03.01.01.14]

Http.Sessions: add the async IHttpSessionStore seam (get/set/renew/remove
keyed by session id, opaque byte[] payload, idle-timeout metadata) with the
in-memory dictionary moved behind it as the default InMemoryHttpSessionStore.
Payload framing is a version-prefixed, length-prefixed binary codec
(HttpSessionSerializer) — no reflection — so any store round-trips identical
bytes. The concurrency contract is last-commit-wins (documented on the
interface + DESIGN.md, pinned by tests); sliding expiration is renew-on-access.
An internal store-backed session composes the store with the frame codec. Every
existing IHttpSession caller and test is unchanged.

Web.Sessions: fill the empty scaffold with the per-request middleware.
UseSessions installs a lazy, store-backed session on the exchange (no store I/O
or cookie until first access), establishes a hardened session-id cookie
(HttpOnly, SameSite=Lax, Secure on HTTPS, session-scoped) via the Http.Cookies
model synchronously at id-mint time (before the head commits), and commits
after next (persist-if-modified, else slide). Session ids are 128-bit
crypto-random base64url; RegenerateSessionIdAsync is the post-auth fixation
defense (new id, old id removed, cookie replaced). Distributed backends are
deferred to IHttpSessionStore adapters.

Wiring: Web.Sessions docs (OVERVIEW/DESIGN), Http.Sessions DESIGN.md scope
update, README project-map row, slnx docs entries. Manifest + CI matrix already
carried the scaffold; App.Web.Runtime pack validated with both assemblies.

Tests: Http.Sessions 47 (23 existing regression + 24 new), Web.Sessions 15
(unit middleware + e2e round-trip over WebApplicationTestFactory). Stack
regressions green (ErrorHandling/Serialization/Compression/HttpsPolicy/
RateLimiting/Hosting).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): never persist an orphaned new session whose cookie could not be delivered

Review follow-ups on the #785 branch: a session first accessed after the response
head commits mints an id the client can never present again — skip the store
persist instead of littering the store until idle-timeout reaping (externally
supplied sessions keep their caller-owned commit lifecycle); pin with a
RecordingSessionStore fixture and an existing-cookie-after-head counterpart
test; true up the stale Http.Sessions csproj description (Items bag -> typed
feature collection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.Caching server-owned output caching [L03.01.01.17]

New resources/Web/Assimalign.Cohesion.Web.Caching feature package: UseOutputCache
serves cacheable GET/HEAD responses from an async, tag-aware store without invoking
the endpoint, buffering the response body on a miss within per-entry/total size caps.

Design:
- Async tag-aware IOutputCacheStore seam (get/set with tags, TTL, opaque entries)
  layered above the sync Caching foundation; default public InMemoryOutputCacheStore
  adapts Caching.InMemory MemoryCache with SizeLimit + per-entry size accounting and
  a self-cleaning tag index (precedent: InMemoryHttpSessionStore).
- Builder-time policy model: base + named policies + per-endpoint overrides via the
  sealed OutputCacheMetadata carrier read last-wins at the router's metadata seam.
  Endpoint discovery uses the router's own side-effect-free Match ahead of UseRouting
  (async-correct; a hit skips the handler) rather than the sync route-match decorator.
- Cache-or-bypass via the #755 typed HttpCacheControl/HttpFreshness primitives:
  bypass on no-store/private/no-cache, Set-Cookie, Authorization (never cache
  authenticated by default), non-safe methods, non-200; TTL = policy Duration capped
  by response freshness.
- Vary: the cache key honors the response's own Vary header (RFC 9111 4.1) via a
  per-primary-key marker + variant entries, so a compressed/negotiated variant is
  never served to a client that cannot accept it; register before UseResponseCompression.
- QUERY (RFC 10008) scoped out (documented follow-up): no non-destructive request-content
  key seam exists yet; cacheable set is GET/HEAD.
- Tag eviction reachable from app code (held store or IOutputCacheFeature).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, docs
(OVERVIEW/DESIGN with the store seam, policy model, bypass matrix, the Vary decision
+ ordering, QUERY posture, size accounting, non-goals), README row. 28 new tests
(store, key builder, middleware bypass matrix, e2e hit/miss/vary/bypass/evict/opt-in).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): never store Set-Cookie in output-cache entries even under CacheAuthenticated

Review follow-up on the #795 branch: the CacheAuthenticated opt-in shares the
response representation, but the Set-Cookie field is per-recipient — a stored
entry replaying one client's cookie grant (e.g. a fresh session id) to every
hit is a session-leak footgun. Set-Cookie joins the unconditional non-cacheable
header set; pinned by a hit-without-Set-Cookie test and documented in DESIGN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dotnetcadet added a commit that referenced this pull request Jul 20, 2026
…er [L03.01.01.01.06] (#927)

* fix(web): import Internal namespace so Web.Authentication builds

Commit 6688b71 renamed HttpAuthenticationFeature -> AuthenticationFeature but
left HttpContextAuthenticationExtensions (namespace Assimalign.Cohesion.Web.Authentication)
without a using for the child .Internal namespace where the concrete type lives,
breaking the Web.Authentication build and every project that references it —
including the Web.Hosting test project this task needs to verify. Add the missing using.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): pipeline exception boundary, status-code pages, 404 terminal via OnError hook [L03.01.01.19]

Implements #881 as a CONSUMER of the shipped #864 OnError chain (PR #893),
re-scoped from the issue's IResult design: IResult was withdrawn pre-merge, so the
Web area is middleware-first and there is no result-carrier channel. No IResult, no
Results.Problem/ExecuteResultAsync, no second handler abstraction.

Web.ErrorHandling (new pipeline verbs, homed with AddErrorHandling):
- UseErrorHandling() installs the exception-boundary middleware: catches faults
  escaping downstream, publishes IHttpExceptionFeature (caught exception + path),
  and on an unstarted response resets it and dispatches through the shipped
  IErrorHandlingFeature.Handlers chain (registration order, first-true wins) to the
  ProblemDetails-500 terminal. No-clobber: when IHttpResponseStreamingFeature
  .HasStarted reports the head is committed, it aborts the one exchange via
  IHttpContext.CancelAsync instead of half-writing (connection survives).
- Developer-detail toggle (off by default) enriches only the boundary's terminal
  fallback. OnException observation hook + SuppressDiagnosticsCallback are the
  Cohesion parity for .NET 10 SuppressDiagnosticsCallback (no Microsoft.Extensions
  logging). Handler faults propagate, never masked (shipped OnError semantics);
  an observer fault is swallowed (observation must not defeat rendering).
- UseStatusCodePages() upgrades a bodyless 4xx/5xx terminal response into
  problem+json (or a custom responder).

Web.Hosting:
- The silent Task.CompletedTask pipeline terminal now sets a bodyless 404 for an
  unhandled request (still 200, no body/content-type/location). It stays payload-
  free because COHRES002 forbids the runtime module referencing Web.ProblemDetails;
  UseStatusCodePages upgrades it. A deliberate empty 200 must be terminal (not chain
  to next) — the TLS integration test's terminal handler is adapted to match.

Tests: 22 new Web.ErrorHandling unit tests + 3 end-to-end (UseErrorHandling default,
handler-owns, UseStatusCodePages 404 upgrade) + 3 Web.Hosting terminal tests. Docs:
Web.ErrorHandling DESIGN/OVERVIEW, Web.Hosting DESIGN, Web README updated; fixed the
IHttpErrorHandler/IHttpErrorHandlingFeature naming drift in OVERVIEW.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): repair Web.Routing test context double and IErrorHandlingFeature naming drift

Review follow-ups on the #881 branch: implement IWebApplicationContext.ContentRootPath
on the Web.Routing TestWebApplicationContext (stale since 6688b71) and correct the
IHttpErrorHandlingFeature -> IErrorHandlingFeature name drift in the ErrorHandling
csproj description, xmldoc, and DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): content negotiation over the serialization registry [L03.01.02.01.02]

Adds server-side content negotiation as a thin layer over the #864
content-serialization registry (#149), reusing the #771
`HttpContentNegotiation` q-value/precedence primitive rather than
re-implementing selection.

- `HttpContentNegotiationExtensions`: `IHttpContentSerializationFeature.TryNegotiate`
  (pure, non-throwing seam over the registered writers), `IHttpContext.TryNegotiateContentType`
  (reads the exchange's Accept), and `IHttpContext.WriteNegotiatedContentAsync<T>`
  (negotiate -> write via the registry's explicit media-type overload, or compose a 406).
- `ContentNegotiator` (internal): collects the writers' concrete media types in
  server-preference order and delegates exact RFC 9110 §12.5.1 matching to the primitive.
- Structured-suffix fallback (the call #864 deferred to #149): a bare base-type Accept
  range (application/json) is satisfied by a registered structured-suffix writer
  (application/problem+json) only when exact matching yields nothing; already-suffixed
  ranges are never widened, q=0 refusals are honored, and `application/*+json` wildcard-suffix
  ranges stay unsupported (a #771 parser gap, recorded not duplicated).
- `Vary: Accept` appended (never clobbering an existing Vary) on negotiated responses and
  the 406; no acceptable representation is a bodyless 406 outcome the #881 status-code-pages
  middleware upgrades, while a missing registry stays the existing composition fault.
- Media types only; Accept-Charset/Language and the client half are non-goals.

DESIGN.md/OVERVIEW.md updated in the same change. 15 new tests (11 unit + 4 pipeline);
Web.Serialization 42, Web.ErrorHandling 37, Web.Hosting 49 all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.Compression response compression + request decompression [L03.01.01.08]

New resources/Web feature package housing both directions of HTTP body
compression over BCL codecs only (GZipStream/BrotliStream/ZLibStream), AOT-safe,
builder-time options, no Microsoft.Extensions.* and no hosting reference.

Response side (UseResponseCompression): negotiates gzip/brotli from Accept-Encoding
via the shared Http #771 primitives, wrapping IHttpResponse.Body with a
first-write-deferred decision stream (there is no header-commit hook). It stamps
Content-Encoding, drops the stale Content-Length so the transport re-synthesizes it,
and always appends Vary: Accept-Encoding for eligible media types (no-clobber over an
existing Accept token). Honors a size threshold without buffering whole responses,
never double-compresses already-encoded content, and hands off cleanly to streamed
responses. Off by default for HTTPS dynamic content (BREACH, CVE-2013-3587) via an
explicit EnableForHttps opt-in. Per-response opt-out via IResponseCompressionFeature.

Request side (UseRequestDecompression): transparently inflates gzip/br/deflate bodies
by decorating the exchange (IHttpRequest.Body is get-only), enforcing a decompressed-
size guard (413) against zip bombs, 415 on unsupported codings, 400 on malformed
content, and decoding multi-coding chains (Content-Encoding: gzip, br) in reverse
application order.

Wiring: App.Web framework manifest, root + Web solutions, resource-web CI matrix,
and the Web README project map. Docs: OVERVIEW.md + DESIGN.md.

Tests: 27 (in-memory factory E2E for both directions incl. round-trips, threshold,
Vary append, 413/415/400, multi-coding; unit tests for the BREACH gate and MIME
matcher). Web.ErrorHandling/Serialization/Hosting stack suites remain green.

No new Http-core primitive was needed: HttpAcceptParser.ParseAcceptEncoding +
HttpContentNegotiation.TrySelectEncoding (#771) already cover content-coding/q-value
parsing and identity;q=0 semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): Web.HttpsPolicy HTTPS redirection + HSTS; fix Strict-Transport-Security header key [L03.01.01.09]

Add resources/Web/Assimalign.Cohesion.Web.HttpsPolicy, one lean feature package
pairing both HTTPS-posture concerns as extension(IWebApplicationPipelineBuilder)
verbs (no DI/config/logging; refs Web + Http only, never Web.Hosting):

- UseHttpsRedirection: insecure request -> bodyless method-preserving redirect
  (307 default, 308 configurable); Location rebuilt from the request (https
  scheme, host with its port swapped to the configured HTTPS port — 443 omitted,
  IPv6 re-bracketed; path verbatim; query reconstructed from the parsed
  collection). HttpsPort is explicit (Web.Hosting isolation forbids deriving it).
- UseHsts: RFC 6797 Strict-Transport-Security composed once at builder time
  (max-age default 365d, includeSubDomains, preload), emitted on secure responses
  only (RFC 6797 7.2), loopback (localhost/127.0.0.1/[::1]) excluded by default
  via the core HttpHostMatcher. Applied post-next (outside the #881 boundary) so
  it survives the boundary's header reset on a faulted response; skipped on a
  committed head via IHttpHeaderCollection.IsReadOnly.

Connection security is read from the transport-derived typed IHttpRequest.Scheme
(#763), not scheme-string sniffing.

Also fix HttpHeaderKey.StrictTransportSecurity value from the misspelled
"Strict-Transports-Security" to "Strict-Transport-Security" (RFC 6797), with
round-trip tests through IHttpHeaderCollection (member name unchanged).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, README map,
OVERVIEW/DESIGN docs. Tests: 32 new (redirection + HSTS) via unit-level context
and pipeline-builder doubles; 3 new core Http header-key tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): note TLS-terminating-proxy behavior for HttpsPolicy scheme detection

Review follow-up: Request.Scheme is the immediate hop; Web.ForwardedHeaders
deliberately never mutates it, so document the plaintext-hop redirect-loop
hazard and the IHttpForwardedFeature follow-up rather than leaving it implicit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.RateLimiting inbound rate-limiting middleware and policies [L03.01.01.12]

New resources/Web feature package adapting the BCL System.Threading.RateLimiting
engine to the Web pipeline — supplying the middleware, policy model, partition-key
surface, and rejection response, never a limiter algorithm (#783).

- UseRateLimiting composes a global limiter (applied to every request, acquired
  up-front with full queueing) plus named policies attached per-endpoint as the
  sealed RateLimitingMetadata carrier in the routing metadata bag.
- Per-endpoint gating mirrors the Web.RequestTimeouts seam: a decorated context
  reads the matched endpoint's metadata at the router's route-match publication.
  Since the router matches and dispatches in one middleware, the sync gate uses
  AttemptAcquire and raises an internal signal the middleware catches to answer
  before the handler runs. Global + endpoint are additive (both must grant a lease).
- Partitioned via PartitionedRateLimiter<IHttpContext> with AOT-safe selectors:
  RateLimitPartitionKeys.ClientAddress composes the Http.Forwarded effective client
  (trust-gated; BCP 38 caution documented), plus Header and typed selectors.
- Rejection: 429 (configurable) + Retry-After from lease metadata, OnRejected hook
  (bodyless default composes with #881 status-code pages), OnDecision observation
  hook in place of a hosting-layer OTel seam (re-scoped, follow-up recorded).
- IRateLimitingFeature exposes the decision downstream. Limiters live for the
  application lifetime; per-request leases released on completion (disposal posture
  documented).

Wiring: App.Web manifest, both slnx, resource-web.yml matrix, README project map,
OVERVIEW/DESIGN docs. System.Threading.RateLimiting is not added to the framework
manifest — it carries only Cohesion assemblies, matching the existing
Resilience.RateLimiting precedent in base App. 20 new tests (15 middleware unit +
5 e2e over WebApplicationTestFactory), all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(web): document RateLimiting signal ordering constraint vs the exception boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): async session-store seam + Web.Sessions middleware [L03.01.01.14]

Http.Sessions: add the async IHttpSessionStore seam (get/set/renew/remove
keyed by session id, opaque byte[] payload, idle-timeout metadata) with the
in-memory dictionary moved behind it as the default InMemoryHttpSessionStore.
Payload framing is a version-prefixed, length-prefixed binary codec
(HttpSessionSerializer) — no reflection — so any store round-trips identical
bytes. The concurrency contract is last-commit-wins (documented on the
interface + DESIGN.md, pinned by tests); sliding expiration is renew-on-access.
An internal store-backed session composes the store with the frame codec. Every
existing IHttpSession caller and test is unchanged.

Web.Sessions: fill the empty scaffold with the per-request middleware.
UseSessions installs a lazy, store-backed session on the exchange (no store I/O
or cookie until first access), establishes a hardened session-id cookie
(HttpOnly, SameSite=Lax, Secure on HTTPS, session-scoped) via the Http.Cookies
model synchronously at id-mint time (before the head commits), and commits
after next (persist-if-modified, else slide). Session ids are 128-bit
crypto-random base64url; RegenerateSessionIdAsync is the post-auth fixation
defense (new id, old id removed, cookie replaced). Distributed backends are
deferred to IHttpSessionStore adapters.

Wiring: Web.Sessions docs (OVERVIEW/DESIGN), Http.Sessions DESIGN.md scope
update, README project-map row, slnx docs entries. Manifest + CI matrix already
carried the scaffold; App.Web.Runtime pack validated with both assemblies.

Tests: Http.Sessions 47 (23 existing regression + 24 new), Web.Sessions 15
(unit middleware + e2e round-trip over WebApplicationTestFactory). Stack
regressions green (ErrorHandling/Serialization/Compression/HttpsPolicy/
RateLimiting/Hosting).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): never persist an orphaned new session whose cookie could not be delivered

Review follow-ups on the #785 branch: a session first accessed after the response
head commits mints an id the client can never present again — skip the store
persist instead of littering the store until idle-timeout reaping (externally
supplied sessions keep their caller-owned commit lifecycle); pin with a
RecordingSessionStore fixture and an existing-cookie-after-head counterpart
test; true up the stale Http.Sessions csproj description (Items bag -> typed
feature collection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): Web.Caching server-owned output caching [L03.01.01.17]

New resources/Web/Assimalign.Cohesion.Web.Caching feature package: UseOutputCache
serves cacheable GET/HEAD responses from an async, tag-aware store without invoking
the endpoint, buffering the response body on a miss within per-entry/total size caps.

Design:
- Async tag-aware IOutputCacheStore seam (get/set with tags, TTL, opaque entries)
  layered above the sync Caching foundation; default public InMemoryOutputCacheStore
  adapts Caching.InMemory MemoryCache with SizeLimit + per-entry size accounting and
  a self-cleaning tag index (precedent: InMemoryHttpSessionStore).
- Builder-time policy model: base + named policies + per-endpoint overrides via the
  sealed OutputCacheMetadata carrier read last-wins at the router's metadata seam.
  Endpoint discovery uses the router's own side-effect-free Match ahead of UseRouting
  (async-correct; a hit skips the handler) rather than the sync route-match decorator.
- Cache-or-bypass via the #755 typed HttpCacheControl/HttpFreshness primitives:
  bypass on no-store/private/no-cache, Set-Cookie, Authorization (never cache
  authenticated by default), non-safe methods, non-200; TTL = policy Duration capped
  by response freshness.
- Vary: the cache key honors the response's own Vary header (RFC 9111 4.1) via a
  per-primary-key marker + variant entries, so a compressed/negotiated variant is
  never served to a client that cannot accept it; register before UseResponseCompression.
- QUERY (RFC 10008) scoped out (documented follow-up): no non-destructive request-content
  key seam exists yet; cacheable set is GET/HEAD.
- Tag eviction reachable from app code (held store or IOutputCacheFeature).

Wiring: App.Web manifest, both slnx files, resource-web.yml matrix, docs
(OVERVIEW/DESIGN with the store seam, policy model, bypass matrix, the Vary decision
+ ordering, QUERY posture, size accounting, non-goals), README row. 28 new tests
(store, key builder, middleware bypass matrix, e2e hit/miss/vary/bypass/evict/opt-in).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): never store Set-Cookie in output-cache entries even under CacheAuthenticated

Review follow-up on the #795 branch: the CacheAuthenticated opt-in shares the
response representation, but the Set-Cookie field is per-recipient — a stored
entry replaying one client's cookie grant (e.g. a fresh session id) to every
hit is a session-leak footgun. Set-Cookie joins the unconditional non-cacheable
header set; pinned by a hit-without-Set-Cookie test and documented in DESIGN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): HTTP/3 (QUIC) registration surface on the Web server builder [L03.01.01.01.06]

Add UseHttp3 to WebHostingExtensions as the h3 counterpart of the TCP
UseHttp1s/UseHttp2s sugar, resolving the async-materialization mismatch the
prior remarks recorded as the reason h3 was omitted. Two overloads:

- UseHttp3(Action<QuicConnectionListenerOptions>) — QUIC-native form; the cert
  flows through ServerAuthenticationOptions (the TlsServerOptions-equivalent).
- UseHttp3(Action<QuicConnectionListenerOptions>, TlsServerOptions) — mirrors
  UseHttp2s ergonomics; the cert flows through the same TlsServerOptions surface.

Both register the transport's existing synchronous deferred-factory seam
(HttpConnectionListenerOptions.UseHttp3(Func<IMultiplexedConnectionListener>))
and materialize the QUIC listener at server start — never at configuration time —
blocking once on QuicConnectionListener.CreateAsync, offloaded to the pool to
avoid any SynchronizationContext deadlock. ALPN defaults to h3 and TLS to 1.3
when unset. Members are [SupportedOSPlatform]-annotated; on a platform without
QUIC (QuicListener.IsSupported == false) materialization throws
PlatformNotSupportedException at start. Coexists with h1/h2 on one listener and
feeds Alt-Svc (#754) advertisement with no extra wiring.

Tests: registration/ALPN/defer/null-arg unit tests plus platform-guarded
materialization, coexistence, and a real-QUIC e2e (server-side Http/3 + https
observation load-bearing; client response best-effort because the full h3
response round-trip trips a pre-existing Http.Connections server control-stream
defect, H3_CLOSED_CRITICAL_STREAM, reproduced by that library's Http3 example).

No new dependencies (Connections.Quic already referenced). Updates DESIGN.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

[L03.01.02.01.08] Content-serialization registry + OnError hook — pipeline formatting and error-handling design (re-scoped from IResult)

1 participant