Skip to content

[L03.01.01.01.04] Rewrite WebApplicationServer: per-connection dispatch, error isolation, disposal, graceful stop #762

Description

@dotnetcadet

Summary

  • The core blocker for wiring Http into the Web resource. WebApplicationServer.StartAsync queues ONE async-void ThreadPool work item whose loop accepts a connection and then inline-awaits its entire ReceiveAsync enumerable: a single idle HTTP/1.1 keep-alive client starves every other connection indefinitely (they pile up in the listener's 512-slot backlog, never served). There is no try/catch (an application exception escapes the async-void body and CRASHES THE PROCESS via thread-pool escalation — and even graceful StopAsync triggers this via OperationCanceledException), connections/contexts are never disposed (violating the transport DESIGN.md disposal contract), and no connection-count limit exists.
  • The server owns application-exception isolation, per-connection dispatch, disposal, in-flight tracking for StopAsync drain, and an optional MaxConcurrentConnections cap; wire-level failure isolation already lives in Http.Connections and must not be duplicated. Web.Hosting has no docs/DESIGN.md — create it in this change (server dispatch model, stop semantics).

Captured 2026-07-03 during the Http/Web requirements audit; every claim below was adversarially verified against the codebase before filing.

Acceptance Criteria

  • With connection A held open as an idle HTTP/1.1 keep-alive connection (one request already served), a request sent on a second connection B receives a complete response within a bounded time (e.g. 2s) — proving accepted connections are served concurrently, not serially.
  • An exception thrown from the middleware pipeline while serving one connection tears down and disposes only that connection; the server loop keeps running, a subsequent new connection is accepted and served, and the process does not crash from an unhandled async-void/thread-pool exception.
  • Every accepted IHttpConnection and opened IHttpConnectionContext is disposed when its per-connection loop ends, whether it ends normally, by client disconnect, or by pipeline fault (verified with an instrumented fake listener/connection).
  • StopAsync cancels the accept loop, awaits in-flight connection tasks (graceful drain), and completes without any unobserved OperationCanceledException or other escaped exception.
  • A MaxConcurrentConnections option on the server (default documented — unlimited or a stated default) is enforced: when the limit is reached, additional accepted connections are not opened/served until an active connection completes, and this is covered by a test.
  • All new behavior is covered by tests in resources/Web/Assimalign.Cohesion.Web.Hosting/tests using xUnit + Shouldly per repo conventions, and the WebApplicationServer no longer uses ThreadPool.UnsafeQueueUserWorkItem with an async lambda.

Standards and Compliance

  • Runtime-contract concern — no wire-protocol conformance applies. AOT-safe (SemaphoreSlim gate, stored Tasks, no reflection); no Microsoft.Extensions.*; per-service execution model per libraries/Hosting docs/DESIGN.md

Implementation notes (from the audit)

Change lands entirely in resources/Web/Assimalign.Cohesion.Web.Hosting (project Assimalign.Cohesion.Web.Hosting.csproj): rewrite WebApplicationServer.StartAsync to accept in a dedicated loop and dispatch each accepted connection to its own tracked Task (per-connection loop = OpenAsync + await foreach ReceiveAsync + pipeline execute/send/dispose, wrapped in try/catch and await using). If a MaxConcurrentConnections knob is exposed, it goes on WebApplicationServerOptions/WebApplicationServerBuilder (SemaphoreSlim gate around accept — AOT-safe, no reflection). Layering boundary: wire-level failure isolation and per-stream RST/GOAWAY handling already live in Assimalign.Cohesion.Http.Connections (its docs/DESIGN.md lines 303-332) and must NOT be duplicated; the server owns only application-exception isolation, per-connection dispatch, connection disposal, in-flight tracking for StopAsync drain, and the optional cap. No transport prerequisites: HttpConnectionListener already buffers accepted connections in a channel and HttpConnectionListenerTests.cs line 40 (AcceptOrListenAsync_OnSequentialAccepts_ShouldBacklogConnections) proves backlogged accepts survive until drained. While there, replace the async-void ThreadPool.UnsafeQueueUserWorkItem(WaitCallback) pattern with a stored Task (process-crash hazard) and make StopAsync await the accept loop + in-flight connections, swallowing only OperationCanceledException. The stale Web.ApplicationModel HttpServer.cs (references nonexistent ITransport) shows the previously intended dispatch shape but is dead code — do not resurrect it; consider deleting separately. Tests go in resources/Web/Assimalign.Cohesion.Web.Hosting/tests (currently empty csproj); use xUnit v2 + Shouldly with the 'Cohesion Test [Web Hosting] - ...' display-name convention. AGENTS.md rules apply: file-scoped namespaces, implementations internal, AOT-compatible, no Microsoft.Extensions.*.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions