From 8f8612755d9c84b881b7184ce3e766030134be69 Mon Sep 17 00:00:00 2001 From: st0o0 <64534642+st0o0@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:57:31 +0200 Subject: [PATCH 1/2] reformat docs --- ARCHITECTURE.md | 162 +++++++++++++++---------------- CLAUDE.md | 7 +- docs/api/index.md | 124 +++++++++++------------ docs/architecture/engines.md | 41 ++++---- docs/architecture/handlers.md | 51 +++++----- docs/architecture/layers.md | 1 + docs/architecture/pipeline.md | 50 +++++----- docs/architecture/scenarios.md | 12 +-- docs/guide/caching.md | 44 ++++----- docs/guide/configuration.md | 110 ++++++++++----------- docs/guide/connection-pooling.md | 2 +- docs/guide/content-encoding.md | 12 +-- docs/guide/cookies.md | 12 +-- docs/guide/http2.md | 2 +- docs/guide/http3.md | 26 ++--- docs/guide/index.md | 21 ++-- docs/guide/migration.md | 40 ++++---- docs/guide/redirects.md | 14 +-- docs/guide/retries.md | 34 +++---- docs/guide/troubleshooting.md | 6 ++ docs/why/index.md | 48 ++++----- 21 files changed, 417 insertions(+), 402 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 39a949064..4771eeff4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -49,15 +49,15 @@ TurboHTTP is a high-performance **.NET 10 HTTP client library** built on **Akka. ## Client Layer -| Component | Responsibility | -|-----------|---------------| -| `ITurboHttpClient` | Public API: `ChannelWriter` + `ChannelReader` + `SendAsync()` | -| `ITurboHttpClientFactory` | Creates named/typed client instances; owns `ConnectionPool` lifetime | -| `ITurboHttpClientBuilder` | Fluent DI configuration surface (extends `IServiceCollection`) | -| `TurboClientOptions` | Per-client config: nested `Http1Options`, `Http2Options`, `Http3Options`; timeouts, TLS certificates | -| `TurboRequestOptions` | Per-request defaults: base address, version, headers | -| `PipelineDescriptor` | Aggregates all optional policies into a single record for pipeline construction | -| `TurboHandler` | User middleware (bridges delegating-handler pattern into the BidiFlow chain) | +| Component | Responsibility | +| ------------------------- | ------------------------------------------------------------------------------------------------------ | +| `ITurboHttpClient` | Public API: `ChannelWriter` + `ChannelReader` + `SendAsync()` | +| `ITurboHttpClientFactory` | Creates named/typed client instances; owns `ConnectionPool` lifetime | +| `ITurboHttpClientBuilder` | Fluent DI configuration surface (extends `IServiceCollection`) | +| `TurboClientOptions` | Per-client config: nested `Http1Options`, `Http2Options`, `Http3Options`; timeouts, TLS certificates | +| `TurboRequestOptions` | Per-request defaults: base address, version, headers | +| `PipelineDescriptor` | Aggregates all optional policies into a single record for pipeline construction | +| `TurboHandler` | User middleware (bridges delegating-handler pattern into the BidiFlow chain) | **Factory pattern** — `ITurboHttpClientFactory` rather than direct construction, enabling named clients with separate configurations and shared `ConnectionPool` instances. @@ -84,16 +84,16 @@ ClientStreamOwnerActor (supervisor) Composed via `Atop` (innermost → outermost): -| Stage | RFC | Effect | -|-------|-----|--------| -| `TracingBidiStage` | — | Root `Activity` per request | -| `HandlerBidiStage` | — | Wraps user `TurboHandler` middleware | -| `RedirectBidiStage` | RFC 9110 §15.4 | Follows 301/302/303/307/308; internal feedback loop | -| `CookieBidiStage` | RFC 6265 §5.3–5.4 | Injects/extracts cookies via `CookieJar` | -| `RetryBidiStage` | RFC 9110 §9.2 | Retries idempotent requests; internal feedback loop | -| `ExpectContinueBidiStage` | RFC 9110 §10.1.1 | Manages `Expect: 100-continue` handshake | -| `CacheBidiStage` | RFC 9111 | Short-circuits on hit; stores responses | -| `ContentEncodingBidiStage` | RFC 9110 §8.4 | Compresses requests / decompresses responses | +| Stage | RFC | Effect | +| -------------------------- | ----------------- | --------------------------------------------------- | +| `TracingBidiStage` | — | Root `Activity` per request | +| `HandlerBidiStage` | — | Wraps user `TurboHandler` middleware | +| `RedirectBidiStage` | RFC 9110 §15.4 | Follows 301/302/303/307/308; internal feedback loop | +| `CookieBidiStage` | RFC 6265 §5.3–5.4 | Injects/extracts cookies via `CookieJar` | +| `RetryBidiStage` | RFC 9110 §9.2 | Retries idempotent requests; internal feedback loop | +| `ExpectContinueBidiStage` | RFC 9110 §10.1.1 | Manages `Expect: 100-continue` handshake | +| `CacheBidiStage` | RFC 9111 | Short-circuits on hit; stores responses | +| `ContentEncodingBidiStage` | RFC 9110 §8.4 | Compresses requests / decompresses responses | Request flows top-to-bottom; response flows bottom-to-top. Only BidiFlows for non-null policies in `PipelineDescriptor` are included. @@ -115,28 +115,28 @@ RequestEnricherStage ### Per-Version Engine Assembly -| Version | Encode path | Decode path | -|---------|------------|------------| -| HTTP/1.0 | `Http10ConnectionStage` (unified encode + decode + correlation) | — | -| HTTP/1.1 | `Http11ConnectionStage` (unified encode + decode + correlation) | — | -| HTTP/2 | `Http20EncoderStage` + `PrependPrefaceStage` + `Request2FrameStage` | `Http20DecoderStage` + `ConnectionStage` + `StreamStage` + `CorrelationStage` + `StreamIdAllocatorStage` | -| HTTP/3 | `Http30EncoderStage` + control/QPACK preface stages + `Request2FrameStage` | `Http30DecoderStage` + `ConnectionStage` + `StreamStage` + `CorrelationStage` + `StreamDemuxStage` + QPACK stream stages | +| Version | Encode path | Decode path | +| -------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| HTTP/1.0 | `Http10ConnectionStage` (unified encode + decode + correlation) | — | +| HTTP/1.1 | `Http11ConnectionStage` (unified encode + decode + correlation) | — | +| HTTP/2 | `Http20EncoderStage` + `PrependPrefaceStage` + `Request2FrameStage` | `Http20DecoderStage` + `ConnectionStage` + `StreamStage` + `CorrelationStage` + `StreamIdAllocatorStage` | +| HTTP/3 | `Http30EncoderStage` + control/QPACK preface stages + `Request2FrameStage` | `Http30DecoderStage` + `ConnectionStage` + `StreamStage` + `CorrelationStage` + `StreamDemuxStage` + QPACK stream stages | ### Builders (Streams Layer Orchestration) -| Builder | Responsibility | -|---------|-----------------| -| `Engine` | Thin orchestrator; wires `ProtocolCoreBuilder` and `FeaturePipelineBuilder` to create the final client-facing flow | -| `ProtocolCoreBuilder` | Owns endpoint grouping (`GroupByRequestEndpoint`), version dispatch via `EndpointDispatchStage`, version-specific engine instantiation, and transport selection via `TransportRegistry` (zero Transport-layer imports) | -| `FeaturePipelineBuilder` | Owns BidiFlow feature stack composition (ContentEncoding, Cache, Expect100, Retry, Cookie, Redirect, Handlers, Tracing) | +| Builder | Responsibility | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Engine` | Thin orchestrator; wires `ProtocolCoreBuilder` and `FeaturePipelineBuilder` to create the final client-facing flow | +| `ProtocolCoreBuilder` | Owns endpoint grouping (`GroupByRequestEndpoint`), version dispatch via `EndpointDispatchStage`, version-specific engine instantiation, and transport selection via `TransportRegistry` (zero Transport-layer imports) | +| `FeaturePipelineBuilder` | Owns BidiFlow feature stack composition (ContentEncoding, Cache, Expect100, Retry, Cookie, Redirect, Handlers, Tracing) | ### Stage Naming Convention -| Shape | Inlet | Outlet | Example | -|-------|-------|--------|---------| -| FlowShape (1 in, 1 out) | `StageName.In` | `StageName.Out` | `"Http11Encoder.In"` | -| FanOutShape | `StageName.In` | `StageName.Out.Role` | `"Redirect.Out.Final"` | -| FanInShape | `StageName.In.Role` | `StageName.Out` | `"Http20Correlation.In.Request"` | +| Shape | Inlet | Outlet | Example | +| ----------------------- | ------------------- | -------------------- | -------------------------------- | +| FlowShape (1 in, 1 out) | `StageName.In` | `StageName.Out` | `"Http11Encoder.In"` | +| FanOutShape | `StageName.In` | `StageName.Out.Role` | `"Redirect.Out.Final"` | +| FanInShape | `StageName.In.Role` | `StageName.Out` | `"Http20Correlation.In.Request"` | PascalCase, no protocol prefix, no `Stage` suffix, semantically named roles, globally unique. @@ -193,16 +193,16 @@ ConnectionStage — connection-level frames (SETTINGS, PING, GOAWAY, WINDOW_UPD ### Business Logic Components -| Component | RFC | Responsibility | -|-----------|-----|---------------| -| `RedirectHandler` | RFC 9110 §15.4 | Redirect following with correct method rewriting | -| `RetryEvaluator` | RFC 9110 §9.2 | Idempotency-based retry decisions | -| `ConnectionReuseEvaluator` | RFC 9112 §9 | Keep-alive vs. close decisions | -| `CookieJar` | RFC 6265 | Domain/path matching, Secure/HttpOnly/SameSite | -| `ContentEncodingDecoder` | RFC 9110 §8.4 | gzip/deflate/brotli decompression | -| `HttpCacheStore` | RFC 9111 | Thread-safe in-memory LRU cache | -| `CacheFreshnessEvaluator` | RFC 9111 | Freshness lifetime calculation | -| `CacheValidationRequestBuilder` | RFC 9111 | Conditional request construction | +| Component | RFC | Responsibility | +| ------------------------------- | -------------- | ------------------------------------------------ | +| `RedirectHandler` | RFC 9110 §15.4 | Redirect following with correct method rewriting | +| `RetryEvaluator` | RFC 9110 §9.2 | Idempotency-based retry decisions | +| `ConnectionReuseEvaluator` | RFC 9112 §9 | Keep-alive vs. close decisions | +| `CookieJar` | RFC 6265 | Domain/path matching, Secure/HttpOnly/SameSite | +| `ContentEncodingDecoder` | RFC 9110 §8.4 | gzip/deflate/brotli decompression | +| `HttpCacheStore` | RFC 9111 | Thread-safe in-memory LRU cache | +| `CacheFreshnessEvaluator` | RFC 9111 | Freshness lifetime calculation | +| `CacheValidationRequestBuilder` | RFC 9111 | Conditional request construction | --- @@ -220,11 +220,11 @@ ConnectionPool └── _evictionTimer (at least one connection kept per host) ``` -| Version | Acquire | Release | Limit | -|---------|---------|---------|-------| -| HTTP/1.0 | Always new | Always dispose | None | -| HTTP/1.1 | Idle queue → wait semaphore → establish | Reusable → idle queue; else dispose + release | 6/host | -| HTTP/2+ | MRU with available stream slots → establish | Decrement streams; dispose at 0 | Unlimited | +| Version | Acquire | Release | Limit | +| -------- | ------------------------------------------- | --------------------------------------------- | --------- | +| HTTP/1.0 | Always new | Always dispose | None | +| HTTP/1.1 | Idle queue → wait semaphore → establish | Reusable → idle queue; else dispose + release | 6/host | +| HTTP/2+ | MRU with available stream slots → establish | Decrement streams; dispose at 0 | Unlimited | ### Channels-Based I/O @@ -271,30 +271,30 @@ Connection lifecycle is managed by `IConnectionScope`: - ≤128 KB → 512 KB pause threshold - ≤1 MB → 2 MB pause threshold -- >1 MB → 2× MaxFrameSize +- > 1 MB → 2× MaxFrameSize --- ## Diagnostics -| Mechanism | API | Purpose | -|-----------|-----|---------| -| `TracingBidiStage` | `ActivitySource("TurboHTTP")` | W3C trace context, root `Activity` per request | -| `TurboHttpDiagnosticListener` | `DiagnosticListener` | Publish/subscribe event bus (compatible with `HttpClient` tooling) | -| `TurboHttpEventSource` | ETW `EventSource` | High-performance structured logging (zero alloc on hot path) | -| `TurboHttpMetrics` | OTel `Meter` | `ConnectionActive`, `ConnectionIdle`, `ConnectionDuration` gauges | -| `DeadlockWatchdogStage` | DEBUG only | Fires `OnDeadlockStall` if no element flows within 10s | +| Mechanism | API | Purpose | +| ----------------------------- | ----------------------------- | ------------------------------------------------------------------ | +| `TracingBidiStage` | `ActivitySource("TurboHTTP")` | W3C trace context, root `Activity` per request | +| `TurboHttpDiagnosticListener` | `DiagnosticListener` | Publish/subscribe event bus (compatible with `HttpClient` tooling) | +| `TurboHttpEventSource` | ETW `EventSource` | High-performance structured logging (zero alloc on hot path) | +| `TurboHttpMetrics` | OTel `Meter` | `ConnectionActive`, `ConnectionIdle`, `ConnectionDuration` gauges | +| `DeadlockWatchdogStage` | DEBUG only | Fires `OnDeadlockStall` if no element flows within 10s | --- ## Testing Structure -| Project | Contents | -|---------|----------| -| `TurboHTTP.Tests` | Unit tests organized by RFC namespace (`RFC9112`, `RFC9113`, `Http3/Security`, …) | -| `TurboHTTP.StreamTests` | Akka.Streams `GraphStage` behavior via `StreamTestBase` | +| Project | Contents | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TurboHTTP.Tests` | Unit tests organized by RFC namespace (`RFC9112`, `RFC9113`, `Http3/Security`, …) | +| `TurboHTTP.StreamTests` | Akka.Streams `GraphStage` behavior via `StreamTestBase` | | `TurboHTTP.IntegrationTests` | End-to-end with Kestrel fixtures (16 integration test files each for H2 and H3 covering: smoke, connection, concurrency, cache, cookie, compression, request compression, redirect, retry, error handling, edge case, feature interaction, handler pipeline, resilience, expect-continue, max stream concurrency) | -| `TurboHTTP.Benchmarks` | BenchmarkDotNet suite (25+ benchmarks) | +| `TurboHTTP.Benchmarks` | BenchmarkDotNet suite (25+ benchmarks) | ### HTTP/3 Test Coverage @@ -307,29 +307,29 @@ All `[Fact]`/`[Theory]` tests carry `DisplayName("RFC-section-cat-nnn: descripti ## Extension Points -| Extension point | How | -|----------------|-----| -| Custom middleware | Implement `TurboHandler`; add via `TurboHttpClientBuilder` | -| Custom BidiFlow stages | Extend `GraphStage>`; wire into `FeaturePipelineBuilder.Build()` | -| Custom encoders/decoders | Replace Protocol-layer implementations (maintain RFC wire compatibility) | -| Custom transport | Implement `ITransportFactory`; register via `TransportRegistry` (production + test injection) | +| Extension point | How | +| --------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Custom middleware | Implement `TurboHandler`; add via `TurboHttpClientBuilder` | +| Custom BidiFlow stages | Extend `GraphStage>`; wire into `FeaturePipelineBuilder.Build()` | +| Custom encoders/decoders | Replace Protocol-layer implementations (maintain RFC wire compatibility) | +| Custom transport | Implement `ITransportFactory`; register via `TransportRegistry` (production + test injection) | | Transport registry override | Inject a `TransportRegistry` into `Engine.CreateFlow()` with alternate or test `ITransportFactory` instances | -| DI registration | `AddTurboHttpClient()` + `ITurboHttpClientBuilder.Services` | +| DI registration | `AddTurboHttpClient()` + `ITurboHttpClientBuilder.Services` | --- ## Implementation Status -| Area | Score | Notes | -|------|-------|-------| -| HTTP/1.0 | 85/100 | Stable | -| HTTP/1.1 | 92/100 | Stable | -| HTTP/2 | 87/100 | Stable | -| HTTP/3 | 88/100 | Frame parsing + QUIC transport fully wired; `Http3Options` configuration (QPACK table size, idle timeout, connection limits, blocked streams) integrated via `ProtocolCoreBuilder`; integration and security test coverage now at parity with HTTP/2 | -| HPACK | 90/100 | Stable | -| QPACK | 40/100 | Decoder only; encoder missing | -| Cookies | 80/100 | Stable | -| Caching | 78/100 | Stable | -| Redirects/Retries | 82/100 | Stable | +| Area | Score | Notes | +| ----------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| HTTP/1.0 | 85/100 | Stable | +| HTTP/1.1 | 92/100 | Stable | +| HTTP/2 | 87/100 | Stable | +| HTTP/3 | 88/100 | Frame parsing + QUIC transport fully wired; `Http3Options` configuration (QPACK table size, idle timeout, connection limits, blocked streams) integrated via `ProtocolCoreBuilder`; integration and security test coverage now at parity with HTTP/2 | +| HPACK | 90/100 | Stable | +| QPACK | 40/100 | Decoder only; encoder missing | +| Cookies | 80/100 | Stable | +| Caching | 78/100 | Stable | +| Redirects/Retries | 82/100 | Stable | **Open gaps**: DoS protection (header size/count limits), redirect loop detection, HTTPS→HTTP downgrade blocking, QPACK encoder, trailer header parsing. diff --git a/CLAUDE.md b/CLAUDE.md index 4c216734a..901fa942d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,6 +77,7 @@ Key vault guides: `Architecture/Guides/10-TEST_CONVENTIONS`, `11-STAGE_PORT_NAMI ## Test Conventions (Quick Reference) New tests use **component-based folders** (`Http10/`, `Http11/`, `Http2/`, etc.) not RFC folders. Key rules: + - `Spec` suffix, `sealed` class, BDD method names: `Subject_should_behavior()` - `[Trait("RFC", "RFC9113-4.1")]` for traceability, `[Fact(Timeout = 5000)]` required - `[Fact(DisplayName = ...)]` is deprecated — method name IS the documentation @@ -92,8 +93,8 @@ Full details: `notes/Architecture/Guides/11-STAGE_PORT_NAMING` ## Custom Agents (`.claude/agents/`) -| Agent | When to use | -|-------|-------------| +| Agent | When to use | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `spec-refactorer` | Refactor test specs: remove non-Protocol RFC traits, validate RFC section refs against Obsidian vault, strip `///` comments outside methods | ## Agent Guidance: dotnet-skills @@ -112,12 +113,14 @@ Use for multi-step reasoning where the full scope isn't clear upfront. The tool step-by-step with the ability to revise, branch, and extend as understanding deepens. **When to use:** + - Complex debugging where the root cause isn't obvious - Architecture/design decisions with multiple trade-offs - RFC compliance analysis requiring cross-referencing multiple sections - Any problem where early assumptions may need revision **How it works:** Call the tool repeatedly, once per thought step. Each call takes: + - `thought` — your current reasoning step (analysis, revision, hypothesis, verification) - `thoughtNumber` / `totalThoughts` — track position; adjust `totalThoughts` up/down as needed - `nextThoughtNeeded` — `true` to continue, `false` when done diff --git a/docs/api/index.md b/docs/api/index.md index e04f1137c..d4caede3b 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -187,78 +187,78 @@ public sealed class TurboClientOptions ### Connection options -| Property | Default | Description | -|---|---|---| -| `BaseAddress` | `null` | Base URI for relative requests | -| `ConnectTimeout` | `15 s` | TCP/QUIC connection timeout | -| `PooledConnectionIdleTimeout` | `90 s` | How long idle connections are kept in the pool | -| `PooledConnectionLifetime` | `infinite` | Maximum lifetime of a pooled connection | -| `MaxEndpointSubstreams` | `256` | Max concurrently active endpoint substreams | +| Property | Default | Description | +| ----------------------------- | ---------- | ---------------------------------------------- | +| `BaseAddress` | `null` | Base URI for relative requests | +| `ConnectTimeout` | `15 s` | TCP/QUIC connection timeout | +| `PooledConnectionIdleTimeout` | `90 s` | How long idle connections are kept in the pool | +| `PooledConnectionLifetime` | `infinite` | Maximum lifetime of a pooled connection | +| `MaxEndpointSubstreams` | `256` | Max concurrently active endpoint substreams | Per-version connection limits are configured on the nested options objects: -| Property | Default | Description | -|---|---|---| -| `Http1.MaxConnectionsPerServer` | `6` | Max concurrent HTTP/1.x connections per host | -| `Http1.MaxPipelineDepth` | `16` | Max pipelined requests per HTTP/1.1 connection | -| `Http2.MaxConnectionsPerServer` | `6` | Max concurrent HTTP/2 connections per host | -| `Http2.MaxConcurrentStreams` | `100` | Max concurrent streams per HTTP/2 connection | -| `Http3.MaxConnectionsPerServer` | `4` | Max concurrent QUIC connections per host | +| Property | Default | Description | +| ------------------------------- | ------- | ---------------------------------------------- | +| `Http1.MaxConnectionsPerServer` | `6` | Max concurrent HTTP/1.x connections per host | +| `Http1.MaxPipelineDepth` | `16` | Max pipelined requests per HTTP/1.1 connection | +| `Http2.MaxConnectionsPerServer` | `6` | Max concurrent HTTP/2 connections per host | +| `Http2.MaxConcurrentStreams` | `100` | Max concurrent streams per HTTP/2 connection | +| `Http3.MaxConnectionsPerServer` | `4` | Max concurrent QUIC connections per host | See [Connection Pooling guide](/guide/connection-pooling) for pool lifecycle details. ### HTTP/1.x options -| Property | Default | Description | -|---|---|---| -| `Http1.MaxConnectionsPerServer` | `6` | Max concurrent TCP connections per host | -| `Http1.MaxPipelineDepth` | `16` | Max pipelined requests per connection | -| `Http1.MaxBatchWeight` | `65536` (64 KiB) | Max batch weight for request encoding | -| `Http1.MaxResponseHeadersLength` | `64` (KB) | Max response header size | -| `Http1.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | -| `Http1.MaxResponseDrainSize` | `1048576` (1 MB) | Max bytes to drain from incomplete response | -| `Http1.ResponseDrainTimeout` | `2 s` | Timeout for draining incomplete response body | +| Property | Default | Description | +| -------------------------------- | ---------------- | --------------------------------------------- | +| `Http1.MaxConnectionsPerServer` | `6` | Max concurrent TCP connections per host | +| `Http1.MaxPipelineDepth` | `16` | Max pipelined requests per connection | +| `Http1.MaxBatchWeight` | `65536` (64 KiB) | Max batch weight for request encoding | +| `Http1.MaxResponseHeadersLength` | `64` (KB) | Max response header size | +| `Http1.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | +| `Http1.MaxResponseDrainSize` | `1048576` (1 MB) | Max bytes to drain from incomplete response | +| `Http1.ResponseDrainTimeout` | `2 s` | Timeout for draining incomplete response body | ### HTTP/2 options -| Property | Default | Description | -|---|---|---| -| `Http2.MaxConnectionsPerServer` | `6` | Max concurrent TCP connections per host | -| `Http2.MaxConcurrentStreams` | `100` | Max concurrent streams per connection | -| `Http2.InitialConnectionWindowSize` | `67108864` (64 MB) | Connection-level flow control window | -| `Http2.InitialStreamWindowSize` | `65535` | Per-stream flow control window | -| `Http2.MaxFrameSize` | `16384` (16 KiB) | Max frame payload size | -| `Http2.HeaderTableSize` | `4096` | HPACK dynamic table size | -| `Http2.MaxBatchWeight` | `262144` (256 KiB) | Max batch weight for frame encoding | -| `Http2.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | -| `Http2.KeepAlivePingDelay` | `infinite` | Delay before sending keep-alive PING | -| `Http2.KeepAlivePingTimeout` | `20 s` | Timeout for PING acknowledgment | -| `Http2.KeepAlivePingPolicy` | `Always` | When to send keep-alive PINGs | +| Property | Default | Description | +| ----------------------------------- | ------------------ | ----------------------------------------- | +| `Http2.MaxConnectionsPerServer` | `6` | Max concurrent TCP connections per host | +| `Http2.MaxConcurrentStreams` | `100` | Max concurrent streams per connection | +| `Http2.InitialConnectionWindowSize` | `67108864` (64 MB) | Connection-level flow control window | +| `Http2.InitialStreamWindowSize` | `65535` | Per-stream flow control window | +| `Http2.MaxFrameSize` | `16384` (16 KiB) | Max frame payload size | +| `Http2.HeaderTableSize` | `4096` | HPACK dynamic table size | +| `Http2.MaxBatchWeight` | `262144` (256 KiB) | Max batch weight for frame encoding | +| `Http2.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | +| `Http2.KeepAlivePingDelay` | `infinite` | Delay before sending keep-alive PING | +| `Http2.KeepAlivePingTimeout` | `20 s` | Timeout for PING acknowledgment | +| `Http2.KeepAlivePingPolicy` | `Always` | When to send keep-alive PINGs | ### HTTP/3 options -| Property | Default | Description | -|---|---|---| -| `Http3.MaxConnectionsPerServer` | `4` | Max concurrent QUIC connections per host | -| `Http3.QpackMaxTableCapacity` | `4096` | QPACK dynamic table size | -| `Http3.QpackBlockedStreams` | `100` | Max streams blocked waiting for QPACK | -| `Http3.MaxFieldSectionSize` | `65536` (64 KiB) | Max header block size | -| `Http3.IdleTimeout` | `30 s` | QUIC idle timeout | -| `Http3.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | -| `Http3.AllowEarlyData` | `false` | Allow QUIC 0-RTT early data | -| `Http3.AllowConnectionMigration` | `true` | Allow QUIC connection migration | -| `Http3.AllowServerPush` | `false` | Allow server push via PUSH_PROMISE | -| `Http3.MaxBatchWeight` | `262144` (256 KiB) | Max batch weight for frame encoding | -| `Http3.EnableAltSvcDiscovery` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | +| Property | Default | Description | +| -------------------------------- | ------------------ | ----------------------------------------- | +| `Http3.MaxConnectionsPerServer` | `4` | Max concurrent QUIC connections per host | +| `Http3.QpackMaxTableCapacity` | `4096` | QPACK dynamic table size | +| `Http3.QpackBlockedStreams` | `100` | Max streams blocked waiting for QPACK | +| `Http3.MaxFieldSectionSize` | `65536` (64 KiB) | Max header block size | +| `Http3.IdleTimeout` | `30 s` | QUIC idle timeout | +| `Http3.MaxReconnectAttempts` | `3` | Max reconnect attempts on connection drop | +| `Http3.AllowEarlyData` | `false` | Allow QUIC 0-RTT early data | +| `Http3.AllowConnectionMigration` | `true` | Allow QUIC connection migration | +| `Http3.AllowServerPush` | `false` | Allow server push via PUSH_PROMISE | +| `Http3.MaxBatchWeight` | `262144` (256 KiB) | Max batch weight for frame encoding | +| `Http3.EnableAltSvcDiscovery` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | ### TLS options -| Property | Default | Description | -|---|---|---| -| `DangerousAcceptAnyServerCertificate` | `false` | Skip all certificate validation — dev/test only | -| `ServerCertificateValidationCallback` | Accept valid certs | Custom TLS certificate validation | -| `ClientCertificates` | `null` | Client certificates for mutual TLS | -| `EnabledSslProtocols` | `SslProtocols.None` (OS default) | TLS protocol versions to permit | +| Property | Default | Description | +| ------------------------------------- | -------------------------------- | ----------------------------------------------- | +| `DangerousAcceptAnyServerCertificate` | `false` | Skip all certificate validation — dev/test only | +| `ServerCertificateValidationCallback` | Accept valid certs | Custom TLS certificate validation | +| `ClientCertificates` | `null` | Client certificates for mutual TLS | +| `EnabledSslProtocols` | `SslProtocols.None` (OS default) | TLS protocol versions to permit | ```csharp // Mutual TLS with a client certificate @@ -389,12 +389,12 @@ See [Content Encoding guide](/guide/content-encoding) for request compression an These types are part of the public API and can be customized via the builder extensions: -| Type | Purpose | Guide | -|---|---|---| -| `CookieJar` | Cookie storage and injection — provided via `.WithCookies()` | [Cookies](/guide/cookies) | -| `CacheStore` | In-memory LRU cache backend — provided via `.WithCache(store)` | [Caching](/guide/caching) | -| `RedirectHandler` | Built-in HTTP redirect handling — controlled via `.WithRedirect()` | [Redirects](/guide/redirects) | -| `RetryEvaluator` | Built-in idempotent method retry — controlled via `.WithRetry()` | [Retries](/guide/retries) | -| `TurboHandler` | Custom request/response middleware — registered via `.AddHandler()` | [Extending the Pipeline](/architecture/extending) | +| Type | Purpose | Guide | +| ----------------- | ---------------------------------------------------------------------- | ------------------------------------------------- | +| `CookieJar` | Cookie storage and injection — provided via `.WithCookies()` | [Cookies](/guide/cookies) | +| `CacheStore` | In-memory LRU cache backend — provided via `.WithCache(store)` | [Caching](/guide/caching) | +| `RedirectHandler` | Built-in HTTP redirect handling — controlled via `.WithRedirect()` | [Redirects](/guide/redirects) | +| `RetryEvaluator` | Built-in idempotent method retry — controlled via `.WithRetry()` | [Retries](/guide/retries) | +| `TurboHandler` | Custom request/response middleware — registered via `.AddHandler()` | [Extending the Pipeline](/architecture/extending) | See the [Configuration guide](/guide/configuration) and [Extending the Pipeline](/architecture/extending) for integration patterns. diff --git a/docs/architecture/engines.md b/docs/architecture/engines.md index 73d669c9a..2c0358289 100644 --- a/docs/architecture/engines.md +++ b/docs/architecture/engines.md @@ -19,11 +19,11 @@ HttpRequestMessage → Http10ConnectionStage → NetworkBufferBatchStage → [Tc TCP → [TcpConnectionStage] → Http10ConnectionStage → HttpResponseMessage ``` -| Component | Role | -|-----------|------| -| `Http10ConnectionStage` | Unified stage: serialises request to wire bytes (sets `Connection: close`), parses the HTTP/1.0 response, and correlates request/response (FIFO, depth 1) | -| `NetworkBufferBatchStage` | Coalesces consecutive outbound network buffers into fewer, larger writes to reduce syscalls | -| `TcpConnectionStage` | TCP transport — acquires a connection lease from the pool, reads/writes bytes | +| Component | Role | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Http10ConnectionStage` | Unified stage: serialises request to wire bytes (sets `Connection: close`), parses the HTTP/1.0 response, and correlates request/response (FIFO, depth 1) | +| `NetworkBufferBatchStage` | Coalesces consecutive outbound network buffers into fewer, larger writes to reduce syscalls | +| `TcpConnectionStage` | TCP transport — acquires a connection lease from the pool, reads/writes bytes | **Notable behaviours:** @@ -48,15 +48,16 @@ HttpRequestMessage → Http11ConnectionStage → NetworkBufferBatchStage → [Tc TCP → [TcpConnectionStage] → Http11ConnectionStage → HttpResponseMessage ``` -| Component | Role | -|-----------|------| -| `Http11ConnectionStage` | Unified stage: serialises request (adds `Host`, `Connection`, `Transfer-Encoding: chunked` as needed), parses HTTP/1.1 responses (handles chunked decoding), correlates request/response (FIFO, depth > 1 enables pipelining), and evaluates keep-alive signals | -| `NetworkBufferBatchStage` | Coalesces consecutive outbound network buffers into fewer, larger writes — correctly handles interleaved control items (connection reuse signals) by flushing the buffer before forwarding them | -| `TcpConnectionStage` | TCP transport with connection reuse — returns leases to the pool on keep-alive, requests new connections on close | +| Component | Role | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Http11ConnectionStage` | Unified stage: serialises request (adds `Host`, `Connection`, `Transfer-Encoding: chunked` as needed), parses HTTP/1.1 responses (handles chunked decoding), correlates request/response (FIFO, depth > 1 enables pipelining), and evaluates keep-alive signals | +| `NetworkBufferBatchStage` | Coalesces consecutive outbound network buffers into fewer, larger writes — correctly handles interleaved control items (connection reuse signals) by flushing the buffer before forwarding them | +| `TcpConnectionStage` | TCP transport with connection reuse — returns leases to the pool on keep-alive, requests new connections on close | **Keep-alive handling:** After decoding each response, `Http11ConnectionStage` evaluates the `Connection` header internally: + - `Connection: keep-alive` (or HTTP/1.1 default) → the connection lease is returned to `ConnectionPool` for reuse - `Connection: close` → the lease is released without returning it to the idle queue; the next request triggers a new connection @@ -81,11 +82,11 @@ HttpRequestMessage → Http20ConnectionStage → NetworkBufferBatchStage → [Tc TCP → [TcpConnectionStage] → Http20ConnectionStage → HttpResponseMessage ``` -| Component | Role | -|-----------|------| -| `Http20ConnectionStage` | Central unified stage: allocates client stream IDs (1, 3, 5, …), HPACK-encodes request headers and emits `HEADERS` + `DATA` frames, handles frame encoding/decoding (9-byte frame header + payload), manages connection-level frames (`SETTINGS`, `PING`, `WINDOW_UPDATE`, `GOAWAY`), tracks connection and stream-level flow control windows, assembles per-stream `HEADERS` + `DATA` frames into `HttpResponseMessage`, and correlates responses by stream ID | -| `NetworkBufferBatchStage` | Coalesces consecutive outbound frame buffers into fewer, larger writes — reducing syscall count under concurrent multiplexed streams; control items are flushed through immediately to preserve frame ordering | -| `TcpConnectionStage` | TCP transport — emits the HTTP/2 connection preface on first connect | +| Component | Role | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Http20ConnectionStage` | Central unified stage: allocates client stream IDs (1, 3, 5, …), HPACK-encodes request headers and emits `HEADERS` + `DATA` frames, handles frame encoding/decoding (9-byte frame header + payload), manages connection-level frames (`SETTINGS`, `PING`, `WINDOW_UPDATE`, `GOAWAY`), tracks connection and stream-level flow control windows, assembles per-stream `HEADERS` + `DATA` frames into `HttpResponseMessage`, and correlates responses by stream ID | +| `NetworkBufferBatchStage` | Coalesces consecutive outbound frame buffers into fewer, larger writes — reducing syscall count under concurrent multiplexed streams; control items are flushed through immediately to preserve frame ordering | +| `TcpConnectionStage` | TCP transport — emits the HTTP/2 connection preface on first connect | **HPACK header compression:** @@ -112,11 +113,11 @@ HttpRequestMessage → Http30ConnectionStage → NetworkBufferBatchStage → [Qu QUIC → [QuicConnectionStage] → Http30ConnectionStage → HttpResponseMessage ``` -| Component | Role | -|-----------|------| -| `Http30ConnectionStage` | Central unified stage: QPACK-encodes request headers, emits `HEADERS` + `DATA` frames over QUIC streams, handles frame encoding/decoding using QUIC variable-length encoding, manages connection-level frames (`SETTINGS`, `GOAWAY`), handles stream multiplexing and lifecycle, assembles per-stream frames into `HttpResponseMessage`, and QPACK-decodes response headers | -| `NetworkBufferBatchStage` | Coalesces consecutive outbound items into fewer, larger writes | -| `QuicConnectionStage` | QUIC transport — acquires a QUIC connection from the pool, writes/reads bytes over QUIC streams | +| Component | Role | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Http30ConnectionStage` | Central unified stage: QPACK-encodes request headers, emits `HEADERS` + `DATA` frames over QUIC streams, handles frame encoding/decoding using QUIC variable-length encoding, manages connection-level frames (`SETTINGS`, `GOAWAY`), handles stream multiplexing and lifecycle, assembles per-stream frames into `HttpResponseMessage`, and QPACK-decodes response headers | +| `NetworkBufferBatchStage` | Coalesces consecutive outbound items into fewer, larger writes | +| `QuicConnectionStage` | QUIC transport — acquires a QUIC connection from the pool, writes/reads bytes over QUIC streams | **QPACK header compression:** diff --git a/docs/architecture/handlers.md b/docs/architecture/handlers.md index 3a3cd4b7a..ffc23217e 100644 --- a/docs/architecture/handlers.md +++ b/docs/architecture/handlers.md @@ -4,14 +4,14 @@ `SocketsHttpHandler` (the default handler since .NET 5) ships with the following features out of the box: -| Feature | Default | Configurable | -|---|---|---| -| Redirect | **on** (`AllowAutoRedirect = true`, max 50) | via `MaxAutomaticRedirections` | -| Decompression | **on** (`DecompressionMethods.All`) | via `AutomaticDecompression` | -| Connection Pooling | **on** (per-host, idle eviction) | via `PooledConnectionLifetime` etc. | -| Cookies | **off** (`UseCookies = false`) | via `CookieContainer` | -| HTTP Caching | **not available** | — (Polly / external library) | -| Retry | **not available** | — (Polly / `AddStandardResilienceHandler`) | +| Feature | Default | Configurable | +| ------------------ | ------------------------------------------- | ------------------------------------------ | +| Redirect | **on** (`AllowAutoRedirect = true`, max 50) | via `MaxAutomaticRedirections` | +| Decompression | **on** (`DecompressionMethods.All`) | via `AutomaticDecompression` | +| Connection Pooling | **on** (per-host, idle eviction) | via `PooledConnectionLifetime` etc. | +| Cookies | **off** (`UseCookies = false`) | via `CookieContainer` | +| HTTP Caching | **not available** | — (Polly / external library) | +| Retry | **not available** | — (Polly / `AddStandardResilienceHandler`) | The `IHttpClientFactory` middleware (`DelegatingHandler`) is an opt-in mechanism — it builds on top of the existing request/response model: @@ -182,6 +182,7 @@ services.AddTurboHttpClient("myapi", options => { ... }) ``` User handlers intentionally run **outside** the feedback loops: + - `ProcessRequest` sees each enriched initial request. Redirect and retry re-entries bypass the handler — they re-enter the pipeline further downstream. - `ProcessResponse` sees only **final** responses — after all redirects and retries have been resolved. No intermediate results, no internal noise. @@ -227,13 +228,13 @@ public sealed class AuthHandler(ITokenProvider tokens) : TurboHandler `TurboClientOptions` remains as **transport configuration** (timeouts, TLS, reconnect intervals). Handler configuration (cookies, cache, retry, redirect, user handlers) moves entirely into the `ITurboHttpClientBuilder` extensions. -| Configuration Type | Where | -|---|---| +| Configuration Type | Where | +| -------------------------------------------------------- | ------------------------------------------------------------------- | | Connection parameters (timeouts, TLS, HTTP/2 frame size) | `TurboClientOptions` via `AddTurboHttpClient(name, options => ...)` | -| Redirect / Retry / Cookie / Cache | `ITurboHttpClientBuilder` extensions (`.WithRedirect()` etc.) | -| User handlers | `ITurboHttpClientBuilder` (`.AddHandler()`) | -| BaseAddress | `TurboClientOptions` | -| DefaultRequestHeaders / DefaultRequestVersion | `ITurboHttpClient` (set on the client instance after creation) | +| Redirect / Retry / Cookie / Cache | `ITurboHttpClientBuilder` extensions (`.WithRedirect()` etc.) | +| User handlers | `ITurboHttpClientBuilder` (`.AddHandler()`) | +| BaseAddress | `TurboClientOptions` | +| DefaultRequestHeaders / DefaultRequestVersion | `ITurboHttpClient` (set on the client instance after creation) | --- @@ -289,14 +290,14 @@ The engine reads this descriptor and wires up only the stages you have actually ## Comparison: HttpClient vs. TurboHTTP -| Aspect | HttpClient | TurboHTTP | -|---|---|---| -| Registration | `services.AddHttpClient("name", ...)` | `services.AddTurboHttpClient("name", ...)` | -| Handlers | `.AddHttpMessageHandler()` | `.AddHandler()` | -| Redirect | on by default | off — opt-in via `.WithRedirect()` | -| Retry | off — Polly via `.AddStandardResilienceHandler()` | off — opt-in via `.WithRetry(policy)` | -| Cache | not available | off — opt-in via `.WithCache(policy)` | -| Cookies | off (SocketsHttpHandler) | off — opt-in via `.WithCookies()` | -| Handler base | `DelegatingHandler` (sync/async, per request) | `TurboHandler` (async, stream-compatible) | -| Factory | `IHttpClientFactory` | `ITurboHttpClientFactory` | -| Typed Clients | `AddHttpClient()` | `AddTurboHttpClient()` | \ No newline at end of file +| Aspect | HttpClient | TurboHTTP | +| ------------- | ------------------------------------------------- | ------------------------------------------ | +| Registration | `services.AddHttpClient("name", ...)` | `services.AddTurboHttpClient("name", ...)` | +| Handlers | `.AddHttpMessageHandler()` | `.AddHandler()` | +| Redirect | on by default | off — opt-in via `.WithRedirect()` | +| Retry | off — Polly via `.AddStandardResilienceHandler()` | off — opt-in via `.WithRetry(policy)` | +| Cache | not available | off — opt-in via `.WithCache(policy)` | +| Cookies | off (SocketsHttpHandler) | off — opt-in via `.WithCookies()` | +| Handler base | `DelegatingHandler` (sync/async, per request) | `TurboHandler` (async, stream-compatible) | +| Factory | `IHttpClientFactory` | `ITurboHttpClientFactory` | +| Typed Clients | `AddHttpClient()` | `AddTurboHttpClient()` | diff --git a/docs/architecture/layers.md b/docs/architecture/layers.md index f0d015372..d067d3e4b 100644 --- a/docs/architecture/layers.md +++ b/docs/architecture/layers.md @@ -15,6 +15,7 @@ Console.WriteLine(response.StatusCode); ``` `SendAsync` behaves like `HttpClient.SendAsync`: + - Takes an `HttpRequestMessage` - Returns a `Task` - Supports `CancellationToken` for cancellation diff --git a/docs/architecture/pipeline.md b/docs/architecture/pipeline.md index 9a8476be7..d8d9efcf8 100644 --- a/docs/architecture/pipeline.md +++ b/docs/architecture/pipeline.md @@ -12,20 +12,20 @@ The pipeline view shows every stage in TurboHTTP's request/response path — the Each `HttpRequestMessage` passes through the following stages before reaching the network: -| # | Stage | What it does | -|---|-------|--------------| -| 1 | Request Enrichment (`RequestEnricher`) | Applies your `BaseAddress`, default HTTP version, and default headers to every request | -| 2 | Tracing (`TracingBidiStage`) | Starts an activity span for observability; records request method, URL, timing, and final status code | -| 3 | User Handlers (`HandlerBidiStage`) | Runs any custom middleware you registered — zero or more, applied outermost-first | -| 4 | Redirect (`RedirectBidiStage`) | Tracks the redirect chain; on a `301`–`308` response, re-enters the pipeline at the Cookie stage so new-URL cookies are injected | -| 5 | Cookie Injection (`CookieBidiStage`) | Looks up matching cookies for the target domain and path and adds a `Cookie` header | -| 6 | Retry (`RetryBidiStage`) | On the request side, attaches retry context; on a transient failure, re-enters below the Cookie stage (same URL, cookies already set) | -| 7 | Expect-Continue (`ExpectContinueBidiStage`) | For requests with large bodies, sends `Expect: 100-continue` and holds the body until the server confirms it will accept it | -| 8 | Cache Lookup (`CacheBidiStage`) | Checks the in-memory cache; on a **cache hit**, returns the cached response immediately — stages 9–12 are skipped entirely | -| 9 | Content Encoding (`ContentEncodingBidiStage`) | Compresses the request body if a compression policy is configured; on the response side, transparently decompresses `gzip`, `deflate`, or Brotli | -| 10 | Alt-Svc Discovery (`AltSvcBidiStage`) | Checks for cached Alt-Svc entries and upgrades the request version if a faster protocol is available; captures Alt-Svc headers from responses | -| 11 | Version Router (`Engine`) | Routes the request to the correct protocol handler based on the requested HTTP version | -| 12 | Protocol ConnectionStage *(per version)* | Unified stage that serialises the request to bytes, parses the response, and correlates request/response — then `NetworkBufferBatchStage` batches the outbound writes and `TcpConnectionStage`/`QuicConnectionStage` handles the network connection | +| # | Stage | What it does | +| --- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Request Enrichment (`RequestEnricher`) | Applies your `BaseAddress`, default HTTP version, and default headers to every request | +| 2 | Tracing (`TracingBidiStage`) | Starts an activity span for observability; records request method, URL, timing, and final status code | +| 3 | User Handlers (`HandlerBidiStage`) | Runs any custom middleware you registered — zero or more, applied outermost-first | +| 4 | Redirect (`RedirectBidiStage`) | Tracks the redirect chain; on a `301`–`308` response, re-enters the pipeline at the Cookie stage so new-URL cookies are injected | +| 5 | Cookie Injection (`CookieBidiStage`) | Looks up matching cookies for the target domain and path and adds a `Cookie` header | +| 6 | Retry (`RetryBidiStage`) | On the request side, attaches retry context; on a transient failure, re-enters below the Cookie stage (same URL, cookies already set) | +| 7 | Expect-Continue (`ExpectContinueBidiStage`) | For requests with large bodies, sends `Expect: 100-continue` and holds the body until the server confirms it will accept it | +| 8 | Cache Lookup (`CacheBidiStage`) | Checks the in-memory cache; on a **cache hit**, returns the cached response immediately — stages 9–12 are skipped entirely | +| 9 | Content Encoding (`ContentEncodingBidiStage`) | Compresses the request body if a compression policy is configured; on the response side, transparently decompresses `gzip`, `deflate`, or Brotli | +| 10 | Alt-Svc Discovery (`AltSvcBidiStage`) | Checks for cached Alt-Svc entries and upgrades the request version if a faster protocol is available; captures Alt-Svc headers from responses | +| 11 | Version Router (`Engine`) | Routes the request to the correct protocol handler based on the requested HTTP version | +| 12 | Protocol ConnectionStage _(per version)_ | Unified stage that serialises the request to bytes, parses the response, and correlates request/response — then `NetworkBufferBatchStage` batches the outbound writes and `TcpConnectionStage`/`QuicConnectionStage` handles the network connection | --- @@ -33,17 +33,17 @@ Each `HttpRequestMessage` passes through the following stages before reaching th After bytes return from the network, the response passes back through the stages in reverse order: -| # | Stage | What it does | -|---|-------|--------------| -| 1 | Protocol ConnectionStage *(per version)* | Parses raw bytes into an `HttpResponseMessage` and matches it to the original request | -| 2 | Alt-Svc Discovery (`AltSvcBidiStage`) | Captures Alt-Svc response headers and caches them for future requests | -| 3 | Content Encoding (`ContentEncodingBidiStage`) | Transparently decompresses `gzip`, `deflate`, or Brotli response bodies | -| 4 | Cache Storage (`CacheBidiStage`) | Saves cacheable responses so future matching requests can be served from memory | -| 5 | Expect-Continue (`ExpectContinueBidiStage`) | Processes `100 Continue` responses and unblocks the request body when the server is ready | -| 6 | Automatic Retry (`RetryBidiStage`) | Re-sends safe (idempotent) requests on transient errors, `408`, or `503` responses; respects `Retry-After` delays | -| 7 | Cookie Storage (`CookieBidiStage`) | Reads `Set-Cookie` headers and stores cookies for future requests | -| 8 | Redirect Following (`RedirectBidiStage`) | Follows `301`–`308` redirects automatically; rewrites the HTTP method where needed; detects loops and blocks HTTPS→HTTP downgrades | -| 9 | Tracing (`TracingBidiStage`) | Closes the activity span, recording the final status code and any errors | +| # | Stage | What it does | +| --- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Protocol ConnectionStage _(per version)_ | Parses raw bytes into an `HttpResponseMessage` and matches it to the original request | +| 2 | Alt-Svc Discovery (`AltSvcBidiStage`) | Captures Alt-Svc response headers and caches them for future requests | +| 3 | Content Encoding (`ContentEncodingBidiStage`) | Transparently decompresses `gzip`, `deflate`, or Brotli response bodies | +| 4 | Cache Storage (`CacheBidiStage`) | Saves cacheable responses so future matching requests can be served from memory | +| 5 | Expect-Continue (`ExpectContinueBidiStage`) | Processes `100 Continue` responses and unblocks the request body when the server is ready | +| 6 | Automatic Retry (`RetryBidiStage`) | Re-sends safe (idempotent) requests on transient errors, `408`, or `503` responses; respects `Retry-After` delays | +| 7 | Cookie Storage (`CookieBidiStage`) | Reads `Set-Cookie` headers and stores cookies for future requests | +| 8 | Redirect Following (`RedirectBidiStage`) | Follows `301`–`308` redirects automatically; rewrites the HTTP method where needed; detects loops and blocks HTTPS→HTTP downgrades | +| 9 | Tracing (`TracingBidiStage`) | Closes the activity span, recording the final status code and any errors | --- diff --git a/docs/architecture/scenarios.md b/docs/architecture/scenarios.md index 07848cc30..7aa89fee2 100644 --- a/docs/architecture/scenarios.md +++ b/docs/architecture/scenarios.md @@ -130,9 +130,9 @@ While request/response streams are active, `Http30ConnectionStage` handles: ### Key Differences from HTTP/2 -| | HTTP/2 | HTTP/3 | -|---|--------|--------| -| **Transport** | TCP + TLS | QUIC (UDP + built-in TLS) | -| **Head-of-line blocking** | Yes — one lost TCP packet stalls all streams | No — each QUIC stream is independent | -| **Header compression** | HPACK | QPACK (adapted for out-of-order delivery) | -| **Connection preface** | Required (`PRI * HTTP/2.0...`) | Not needed — QUIC handles this | +| | HTTP/2 | HTTP/3 | +| ------------------------- | -------------------------------------------- | ----------------------------------------- | +| **Transport** | TCP + TLS | QUIC (UDP + built-in TLS) | +| **Head-of-line blocking** | Yes — one lost TCP packet stalls all streams | No — each QUIC stream is independent | +| **Header compression** | HPACK | QPACK (adapted for out-of-order delivery) | +| **Connection preface** | Required (`PRI * HTTP/2.0...`) | Not needed — QUIC handles this | diff --git a/docs/guide/caching.md b/docs/guide/caching.md index 5678a07f7..b3d6046e8 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -22,29 +22,29 @@ Responses to `POST`, `PUT`, `DELETE`, and all other methods are **never cached** Freshness is evaluated in this priority order: -| Source | Example | Notes | -|--------|---------|-------| -| `s-maxage` directive | `Cache-Control: s-maxage=3600` | Shared-cache lifetime; takes priority over `max-age` | -| `max-age` directive | `Cache-Control: max-age=300` | Seconds from the response date | -| `Expires` header | `Expires: Fri, 21 Mar 2026 12:00:00 GMT` | Absolute expiry date; ignored when `max-age` is present | -| Heuristic freshness | _(no directive)_ | When the server provides no explicit cache lifetime, TurboHTTP estimates one: if a resource was last changed 100 days ago, it is assumed fresh for 10 days (10% of the time since the last modification). This only applies when no `max-age`, `s-maxage`, or `Expires` header is present. | +| Source | Example | Notes | +| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `s-maxage` directive | `Cache-Control: s-maxage=3600` | Shared-cache lifetime; takes priority over `max-age` | +| `max-age` directive | `Cache-Control: max-age=300` | Seconds from the response date | +| `Expires` header | `Expires: Fri, 21 Mar 2026 12:00:00 GMT` | Absolute expiry date; ignored when `max-age` is present | +| Heuristic freshness | _(no directive)_ | When the server provides no explicit cache lifetime, TurboHTTP estimates one: if a resource was last changed 100 days ago, it is assumed fresh for 10 days (10% of the time since the last modification). This only applies when no `max-age`, `s-maxage`, or `Expires` header is present. | Once a cached response becomes stale, TurboHTTP issues a **conditional request** to revalidate it rather than fetching the full response again (see [Conditional Requests](#conditional-requests) below). ## Cache-Control Directives -| Directive | Direction | Behavior | -|-----------|-----------|----------| -| `max-age=N` | Response | Cache for N seconds from the response date | -| `s-maxage=N` | Response | Shared-cache lifetime; overrides `max-age` | -| `no-store` | Response | Never cache this response | -| `no-cache` | Response | Cache the response, but **always revalidate** with the server before serving it | -| `must-revalidate` | Response | Once stale, do not serve the cached copy without revalidation | -| `private` | Response | Do not cache — response is personalised to one user | -| `public` | Response | Explicitly marks the response as cacheable, even on shared caches | -| `no-cache` | Request | Bypass cache; fetch a fresh response from the server | -| `no-store` | Request | Bypass cache and do not store the response | -| `only-if-cached` | Request | Return cached copy or `504 Gateway Timeout` — never go to the network | +| Directive | Direction | Behavior | +| ----------------- | --------- | ------------------------------------------------------------------------------- | +| `max-age=N` | Response | Cache for N seconds from the response date | +| `s-maxage=N` | Response | Shared-cache lifetime; overrides `max-age` | +| `no-store` | Response | Never cache this response | +| `no-cache` | Response | Cache the response, but **always revalidate** with the server before serving it | +| `must-revalidate` | Response | Once stale, do not serve the cached copy without revalidation | +| `private` | Response | Do not cache — response is personalised to one user | +| `public` | Response | Explicitly marks the response as cacheable, even on shared caches | +| `no-cache` | Request | Bypass cache; fetch a fresh response from the server | +| `no-store` | Request | Bypass cache and do not store the response | +| `only-if-cached` | Request | Return cached copy or `504 Gateway Timeout` — never go to the network | ## Conditional Requests @@ -70,10 +70,10 @@ Client Server Two standard mechanisms are used: -| Validator | Request header | Response header | Description | -|-----------|----------------|-----------------|-------------| -| ETag | `If-None-Match: "abc123"` | `ETag: "abc123"` | Opaque token identifying the specific version of the content | -| Last-Modified date | `If-Modified-Since: Mon, 20 Mar 2026 10:00:00 GMT` | `Last-Modified: Mon, 20 Mar 2026 10:00:00 GMT` | Timestamp of the last content modification | +| Validator | Request header | Response header | Description | +| ------------------ | -------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ | +| ETag | `If-None-Match: "abc123"` | `ETag: "abc123"` | Opaque token identifying the specific version of the content | +| Last-Modified date | `If-Modified-Since: Mon, 20 Mar 2026 10:00:00 GMT` | `Last-Modified: Mon, 20 Mar 2026 10:00:00 GMT` | Timestamp of the last content modification | When the server responds with `304 Not Modified`, TurboHTTP: diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 531eb5688..8b1be8382 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -59,9 +59,9 @@ var internalClient = factory.CreateClient("internal"); ### Base Address -| Property | Type | Default | -|----------|------|---------| -| `BaseAddress` | `Uri?` | `null` | +| Property | Type | Default | +| ------------- | ------ | ------- | +| `BaseAddress` | `Uri?` | `null` | Relative `HttpRequestMessage` URIs are resolved against this base address. When `null`, all request URIs must be absolute. @@ -71,12 +71,12 @@ options.BaseAddress = new Uri("https://api.example.com/v2/"); ### Connection Pool -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `ConnectTimeout` | `TimeSpan` | `00:00:15` | Timeout for establishing a new TCP connection | +| Property | Type | Default | Description | +| ----------------------------- | ---------- | ---------- | ------------------------------------------------- | +| `ConnectTimeout` | `TimeSpan` | `00:00:15` | Timeout for establishing a new TCP connection | | `PooledConnectionIdleTimeout` | `TimeSpan` | `00:01:30` | Time a connection may remain idle before eviction | -| `PooledConnectionLifetime` | `TimeSpan` | `infinite` | Maximum lifetime of a pooled connection | -| `MaxEndpointSubstreams` | `uint` | `256` | Maximum concurrently active endpoint substreams | +| `PooledConnectionLifetime` | `TimeSpan` | `infinite` | Maximum lifetime of a pooled connection | +| `MaxEndpointSubstreams` | `uint` | `256` | Maximum concurrently active endpoint substreams | ```csharp options.ConnectTimeout = TimeSpan.FromSeconds(5); @@ -88,13 +88,13 @@ options.PooledConnectionLifetime = TimeSpan.FromMinutes(10); Per-version connection and protocol settings are configured on nested sub-objects: -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `Http1.MaxConnectionsPerServer` | `int` | `6` | Maximum concurrent HTTP/1.x connections per host | -| `Http1.MaxPipelineDepth` | `int` | `16` | Maximum pipelined requests per HTTP/1.1 connection | -| `Http1.MaxBatchWeight` | `int` | `65536` (64 KiB) | Max batch weight for request encoding | -| `Http1.MaxResponseHeadersLength` | `int` | `64` (KB) | Max response header size | -| `Http1.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | +| Property | Type | Default | Description | +| -------------------------------- | ----- | ---------------- | -------------------------------------------------- | +| `Http1.MaxConnectionsPerServer` | `int` | `6` | Maximum concurrent HTTP/1.x connections per host | +| `Http1.MaxPipelineDepth` | `int` | `16` | Maximum pipelined requests per HTTP/1.1 connection | +| `Http1.MaxBatchWeight` | `int` | `65536` (64 KiB) | Max batch weight for request encoding | +| `Http1.MaxResponseHeadersLength` | `int` | `64` (KB) | Max response header size | +| `Http1.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | ```csharp options.Http1.MaxConnectionsPerServer = 12; // raise for parallel HTTP/1.1 @@ -103,14 +103,14 @@ options.Http1.MaxPipelineDepth = 32; ### HTTP/2 Options -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `Http2.MaxConnectionsPerServer` | `int` | `6` | Maximum concurrent HTTP/2 connections per host | -| `Http2.MaxConcurrentStreams` | `int` | `100` | Maximum concurrent streams per connection | -| `Http2.MaxFrameSize` | `int` | `16384` (16 KiB) | Maximum HTTP/2 frame payload size | -| `Http2.HeaderTableSize` | `int` | `4096` | HPACK dynamic table size | -| `Http2.MaxBatchWeight` | `int` | `262144` (256 KiB) | Max batch weight for frame encoding | -| `Http2.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | +| Property | Type | Default | Description | +| ------------------------------- | ----- | ------------------ | ---------------------------------------------- | +| `Http2.MaxConnectionsPerServer` | `int` | `6` | Maximum concurrent HTTP/2 connections per host | +| `Http2.MaxConcurrentStreams` | `int` | `100` | Maximum concurrent streams per connection | +| `Http2.MaxFrameSize` | `int` | `16384` (16 KiB) | Maximum HTTP/2 frame payload size | +| `Http2.HeaderTableSize` | `int` | `4096` | HPACK dynamic table size | +| `Http2.MaxBatchWeight` | `int` | `262144` (256 KiB) | Max batch weight for frame encoding | +| `Http2.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | Increase frame size for workloads with large response bodies to reduce framing overhead: @@ -120,30 +120,30 @@ options.Http2.MaxFrameSize = 4 * 1024 * 1024; // 4 MiB (default: 16 KiB) ### HTTP/3 Options -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `Http3.MaxConnectionsPerServer` | `int` | `4` | Maximum concurrent QUIC connections per host | -| `Http3.QpackMaxTableCapacity` | `int` | `4096` | QPACK dynamic table size | -| `Http3.QpackBlockedStreams` | `int` | `100` | Max streams blocked waiting for QPACK | -| `Http3.MaxFieldSectionSize` | `int` | `65536` (64 KiB) | Max header block size | -| `Http3.IdleTimeout` | `TimeSpan` | `00:00:30` | QUIC idle timeout | -| `Http3.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | -| `Http3.AllowEarlyData` | `bool` | `false` | Allow QUIC 0-RTT early data | -| `Http3.AllowConnectionMigration` | `bool` | `true` | Allow QUIC connection migration | -| `Http3.AllowServerPush` | `bool` | `false` | Allow server push via PUSH_PROMISE | -| `Http3.MaxBatchWeight` | `int` | `262144` (256 KiB) | Max batch weight for frame encoding | -| `Http3.EnableAltSvcDiscovery` | `bool` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | +| Property | Type | Default | Description | +| -------------------------------- | ---------- | ------------------ | -------------------------------------------- | +| `Http3.MaxConnectionsPerServer` | `int` | `4` | Maximum concurrent QUIC connections per host | +| `Http3.QpackMaxTableCapacity` | `int` | `4096` | QPACK dynamic table size | +| `Http3.QpackBlockedStreams` | `int` | `100` | Max streams blocked waiting for QPACK | +| `Http3.MaxFieldSectionSize` | `int` | `65536` (64 KiB) | Max header block size | +| `Http3.IdleTimeout` | `TimeSpan` | `00:00:30` | QUIC idle timeout | +| `Http3.MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | +| `Http3.AllowEarlyData` | `bool` | `false` | Allow QUIC 0-RTT early data | +| `Http3.AllowConnectionMigration` | `bool` | `true` | Allow QUIC connection migration | +| `Http3.AllowServerPush` | `bool` | `false` | Allow server push via PUSH_PROMISE | +| `Http3.MaxBatchWeight` | `int` | `262144` (256 KiB) | Max batch weight for frame encoding | +| `Http3.EnableAltSvcDiscovery` | `bool` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | See [HTTP/3 & QUIC guide](./http3) for QUIC-specific configuration and Alt-Svc discovery. ### TLS / HTTPS -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `DangerousAcceptAnyServerCertificate` | `bool` | `false` | Accept all server certificates — dev/test only | -| `ServerCertificateValidationCallback` | `RemoteCertificateValidationCallback?` | Accept `SslPolicyErrors.None` only | Custom certificate validation | -| `ClientCertificates` | `X509CertificateCollection?` | `null` | Client certificates for mutual TLS | -| `EnabledSslProtocols` | `SslProtocols` | `SslProtocols.None` | TLS versions to enable (`None` = OS default) | +| Property | Type | Default | Description | +| ------------------------------------- | -------------------------------------- | ---------------------------------- | ---------------------------------------------- | +| `DangerousAcceptAnyServerCertificate` | `bool` | `false` | Accept all server certificates — dev/test only | +| `ServerCertificateValidationCallback` | `RemoteCertificateValidationCallback?` | Accept `SslPolicyErrors.None` only | Custom certificate validation | +| `ClientCertificates` | `X509CertificateCollection?` | `null` | Client certificates for mutual TLS | +| `EnabledSslProtocols` | `SslProtocols` | `SslProtocols.None` | TLS versions to enable (`None` = OS default) | ```csharp // Accept a specific self-signed certificate by thumbprint (dev/staging) @@ -187,10 +187,10 @@ builder.Services.AddTurboHttpClient("full-featured", options => **`RedirectOptions` properties:** -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `MaxRedirects` | `int` | `10` | Maximum redirects before throwing an exception | -| `AllowHttpsToHttpDowngrade` | `bool` | `false` | Allow redirects from HTTPS to HTTP | +| Property | Type | Default | Description | +| --------------------------- | ------ | ------- | ---------------------------------------------- | +| `MaxRedirects` | `int` | `10` | Maximum redirects before throwing an exception | +| `AllowHttpsToHttpDowngrade` | `bool` | `false` | Allow redirects from HTTPS to HTTP | See [Redirects guide](./redirects) for method rewriting and security behaviours. @@ -203,10 +203,10 @@ See [Redirects guide](./redirects) for method rewriting and security behaviours. **`RetryOptions` properties:** -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `MaxRetries` | `int` | `3` | Maximum retry attempts per request | -| `RespectRetryAfter` | `bool` | `true` | Honour the server's `Retry-After` header | +| Property | Type | Default | Description | +| ------------------- | ------ | ------- | ---------------------------------------- | +| `MaxRetries` | `int` | `3` | Maximum retry attempts per request | +| `RespectRetryAfter` | `bool` | `true` | Honour the server's `Retry-After` header | See [Automatic Retries guide](./retries) for which methods and status codes trigger retries. @@ -228,11 +228,11 @@ builder.Services.AddTurboHttpClient("client-b", options => { ... }).WithCache(sh **`CacheOptions` properties:** -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `MaxEntries` | `int` | `1000` | Maximum entries in the LRU store | -| `MaxBodyBytes` | `long` | `52428800` (50 MiB) | Maximum body size per cached response | -| `SharedCache` | `bool` | `false` | When `true`, acts as a shared (proxy) cache | +| Property | Type | Default | Description | +| -------------- | ------ | ------------------- | ------------------------------------------- | +| `MaxEntries` | `int` | `1000` | Maximum entries in the LRU store | +| `MaxBodyBytes` | `long` | `52428800` (50 MiB) | Maximum body size per cached response | +| `SharedCache` | `bool` | `false` | When `true`, acts as a shared (proxy) cache | See [HTTP Caching guide](./caching) for freshness evaluation and conditional request behaviour. diff --git a/docs/guide/connection-pooling.md b/docs/guide/connection-pooling.md index 411400d90..4dcf1ad41 100644 --- a/docs/guide/connection-pooling.md +++ b/docs/guide/connection-pooling.md @@ -19,10 +19,10 @@ The pool runs entirely in the background. Your code just calls `SendAsync` — c How connections are reused depends on the HTTP version: +- **HTTP/1.0** — connections are closed after each response. No reuse. Each request opens a new TCP connection. - **HTTP/1.1** — connections use keep-alive by default. After a response is received, the connection returns to the idle pool and is available for the next request. - **HTTP/2** — a single TCP connection carries multiple concurrent requests as independent streams. When a connection reaches its stream limit, additional connections are opened. - **HTTP/3** — similar to HTTP/2 but over QUIC instead of TCP. Supports connection migration and 0-RTT early data. -- **HTTP/1.0** — connections are closed after each response. No reuse. Each request opens a new TCP connection. ## Idle Connection Eviction diff --git a/docs/guide/content-encoding.md b/docs/guide/content-encoding.md index 14bd12bd5..c51303301 100644 --- a/docs/guide/content-encoding.md +++ b/docs/guide/content-encoding.md @@ -4,12 +4,12 @@ TurboHTTP automatically decompresses compressed HTTP responses. When a server se ## Supported Encodings -| Encoding | Header token | Notes | -|----------|-------------|-------| -| Gzip | `gzip`, `x-gzip` | Most common; used by the majority of web servers | -| Deflate | `deflate` | Handles both zlib-wrapped and raw deflate formats | -| Brotli | `br` | Best compression ratio; requires modern server support | -| Identity | `identity` | No compression; body passed through unchanged | +| Encoding | Header token | Notes | +| -------- | ---------------- | ------------------------------------------------------ | +| Gzip | `gzip`, `x-gzip` | Most common; used by the majority of web servers | +| Deflate | `deflate` | Handles both zlib-wrapped and raw deflate formats | +| Brotli | `br` | Best compression ratio; requires modern server support | +| Identity | `identity` | No compression; body passed through unchanged | ## How It Works diff --git a/docs/guide/cookies.md b/docs/guide/cookies.md index 4c2aea424..5fd8c9a0d 100644 --- a/docs/guide/cookies.md +++ b/docs/guide/cookies.md @@ -71,12 +71,12 @@ Set-Cookie: session=xyz; HttpOnly Controls whether a cookie is sent with cross-site requests. TurboHTTP stores the `SameSite` attribute but does **not** enforce it — the library always sends cookies that match domain and path rules. SameSite enforcement is a browser-level protection that does not apply to programmatic HTTP clients. -| Value | Meaning | -|-------|---------| -| `Strict` | Cookie sent only for requests originating from the same site | -| `Lax` | Cookie sent for same-site and top-level cross-site navigations | -| `None` | Cookie sent with all requests (requires `Secure`) | -| _(absent)_ | No policy; treated like `Lax` in browsers | +| Value | Meaning | +| ---------- | -------------------------------------------------------------- | +| `Strict` | Cookie sent only for requests originating from the same site | +| `Lax` | Cookie sent for same-site and top-level cross-site navigations | +| `None` | Cookie sent with all requests (requires `Secure`) | +| _(absent)_ | No policy; treated like `Lax` in browsers | ## Expiration diff --git a/docs/guide/http2.md b/docs/guide/http2.md index 19356c293..649c9606e 100644 --- a/docs/guide/http2.md +++ b/docs/guide/http2.md @@ -17,7 +17,7 @@ If you send one request at a time and wait for each response before sending the With HTTP/1.1, each request occupies an entire TCP connection from start to finish. To run 10 requests in parallel you need 10 separate connections. -With HTTP/2, a single TCP connection carries multiple requests at the same time as independent *streams*. Each stream has its own ID and flows alongside the others without blocking: +With HTTP/2, a single TCP connection carries multiple requests at the same time as independent _streams_. Each stream has its own ID and flows alongside the others without blocking: ``` HTTP/1.1 (4 connections needed for 4 parallel requests): diff --git a/docs/guide/http3.md b/docs/guide/http3.md index 1087b82f4..1859ec953 100644 --- a/docs/guide/http3.md +++ b/docs/guide/http3.md @@ -52,19 +52,19 @@ builder.Services.AddTurboHttpClient("http3-api", options => ### All HTTP/3 options -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `MaxConnectionsPerServer` | `int` | `4` | Max concurrent QUIC connections per host | -| `QpackMaxTableCapacity` | `int` | `4096` | QPACK dynamic table size in bytes | -| `QpackBlockedStreams` | `int` | `100` | Max streams blocked waiting for QPACK encoder | -| `MaxFieldSectionSize` | `int` | `65536` (64 KiB) | Max header block size | -| `IdleTimeout` | `TimeSpan` | `30 s` | QUIC idle timeout | -| `MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | -| `AllowEarlyData` | `bool` | `false` | Allow QUIC 0-RTT early data | -| `AllowConnectionMigration` | `bool` | `true` | Allow QUIC connection migration | -| `AllowServerPush` | `bool` | `false` | Allow server push via PUSH_PROMISE | -| `MaxBatchWeight` | `long` | `262144` (256 KiB) | Max batch weight for frame encoding | -| `EnableAltSvcDiscovery` | `bool` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | +| Property | Type | Default | Description | +| -------------------------- | ---------- | ------------------ | --------------------------------------------- | +| `MaxConnectionsPerServer` | `int` | `4` | Max concurrent QUIC connections per host | +| `QpackMaxTableCapacity` | `int` | `4096` | QPACK dynamic table size in bytes | +| `QpackBlockedStreams` | `int` | `100` | Max streams blocked waiting for QPACK encoder | +| `MaxFieldSectionSize` | `int` | `65536` (64 KiB) | Max header block size | +| `IdleTimeout` | `TimeSpan` | `30 s` | QUIC idle timeout | +| `MaxReconnectAttempts` | `int` | `3` | Max reconnect attempts on connection drop | +| `AllowEarlyData` | `bool` | `false` | Allow QUIC 0-RTT early data | +| `AllowConnectionMigration` | `bool` | `true` | Allow QUIC connection migration | +| `AllowServerPush` | `bool` | `false` | Allow server push via PUSH_PROMISE | +| `MaxBatchWeight` | `long` | `262144` (256 KiB) | Max batch weight for frame encoding | +| `EnableAltSvcDiscovery` | `bool` | `false` | Auto-discover HTTP/3 via Alt-Svc headers | ## 0-RTT Early Data diff --git a/docs/guide/index.md b/docs/guide/index.md index f4f4953b7..9011d7bac 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -50,6 +50,7 @@ ChannelReader responseReader = client.Responses; ``` Use the channel API when: + - You have a producer loop generating requests faster than you can await responses - You want to decouple request creation from response processing - You are integrating TurboHTTP into a pipeline that already uses `System.Threading.Channels` @@ -99,16 +100,16 @@ The channel has a bounded capacity. If the connection cannot keep up with your p TurboHTTP works out of the box — no middleware to wire up, no Polly policies to configure. -| Feature | Description | -|---------|-------------| -| **HTTP/1.0, HTTP/1.1, HTTP/2 & HTTP/3** | Automatic version negotiation; HTTP/2 multiplexes over TCP, HTTP/3 multiplexes over QUIC | -| **Automatic Retries** | Idempotent methods (GET, PUT, DELETE) are retried automatically; respects `Retry-After` headers | -| **Built-in Caching** | In-memory LRU cache with `ETag`/`Last-Modified` conditional requests and `Vary` support | -| **Redirect Following** | Follows 301/302/303/307/308 with correct method rewriting, loop detection, and auth header stripping | -| **Cookie Management** | `CookieJar` stores `Set-Cookie` responses and injects cookies on subsequent requests automatically | -| **Content Encoding** | Automatic gzip, deflate, and Brotli decompression | -| **Connection Pooling** | Per-host pools with idle eviction, automatic reconnect, and configurable concurrency limits | -| **Channel-based API** | `ChannelWriter`/`ChannelReader` interface for backpressure-aware, high-throughput request pipelines | +| Feature | Description | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| **HTTP/1.0, HTTP/1.1, HTTP/2 & HTTP/3** | Automatic version negotiation; HTTP/2 multiplexes over TCP, HTTP/3 multiplexes over QUIC | +| **Automatic Retries** | Idempotent methods (GET, PUT, DELETE) are retried automatically; respects `Retry-After` headers | +| **Built-in Caching** | In-memory LRU cache with `ETag`/`Last-Modified` conditional requests and `Vary` support | +| **Redirect Following** | Follows 301/302/303/307/308 with correct method rewriting, loop detection, and auth header stripping | +| **Cookie Management** | `CookieJar` stores `Set-Cookie` responses and injects cookies on subsequent requests automatically | +| **Content Encoding** | Automatic gzip, deflate, and Brotli decompression | +| **Connection Pooling** | Per-host pools with idle eviction, automatic reconnect, and configurable concurrency limits | +| **Channel-based API** | `ChannelWriter`/`ChannelReader` interface for backpressure-aware, high-throughput request pipelines | ## Next Steps diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 1b0747e8b..6c655bd5e 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -4,16 +4,16 @@ This guide shows how to migrate common `HttpClient` patterns to TurboHTTP. The A ## Quick Comparison -| HttpClient | TurboHTTP | -|---|---| -| `new HttpClient()` | `factory.CreateClient("name")` via DI | -| `IHttpClientFactory` | `ITurboHttpClientFactory` | -| `services.AddHttpClient()` | `services.AddTurboHttpClient()` | -| `client.GetAsync(url)` | `client.SendAsync(new HttpRequestMessage(Get, url), ct)` | -| `DelegatingHandler` | `TurboHandler` (stream-compatible) | -| Polly retry policies | Built-in `.WithRetry()` | -| No caching | Built-in `.WithCache()` | -| `CookieContainer` (manual) | `CookieJar` (automatic) | +| HttpClient | TurboHTTP | +| -------------------------- | -------------------------------------------------------- | +| `new HttpClient()` | `factory.CreateClient("name")` via DI | +| `IHttpClientFactory` | `ITurboHttpClientFactory` | +| `services.AddHttpClient()` | `services.AddTurboHttpClient()` | +| `client.GetAsync(url)` | `client.SendAsync(new HttpRequestMessage(Get, url), ct)` | +| `DelegatingHandler` | `TurboHandler` (stream-compatible) | +| Polly retry policies | Built-in `.WithRetry()` | +| No caching | Built-in `.WithCache()` | +| `CookieContainer` (manual) | `CookieJar` (automatic) | ## Basic Request @@ -43,6 +43,7 @@ var body = await response.Content.ReadAsStringAsync(); ``` Key differences: + - Client is obtained from `ITurboHttpClientFactory`, not constructed directly - Always pass `CancellationToken` explicitly - No shorthand methods like `GetAsync` — use `SendAsync` with `HttpRequestMessage` @@ -107,6 +108,7 @@ builder.Services.AddTurboHttpClient("my-api", options => ``` No Polly dependency needed. TurboHTTP automatically: + - Retries only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) - Never retries POST or PATCH - Respects `Retry-After` headers @@ -242,15 +244,15 @@ Same API. TurboHTTP additionally respects `CancellationToken` at every layer. By switching to TurboHTTP, these features work out of the box without additional libraries: -| Feature | HttpClient Approach | TurboHTTP | -|---|---|---| -| Retries | Polly + DelegatingHandler | Built-in `.WithRetry()` | -| Caching | Custom DelegatingHandler or nothing | Built-in `.WithCache()` | -| Cookies | Manual CookieContainer setup | Automatic `CookieJar` | -| Decompression | `AutomaticDecompression` flag | Automatic (gzip, deflate, brotli) | -| Connection pooling | SocketsHttpHandler (opaque) | Actor-based, per-host, configurable | -| Backpressure | None | End-to-end via Akka.Streams | -| Channel API | None | `ChannelWriter` / `ChannelReader` | +| Feature | HttpClient Approach | TurboHTTP | +| ------------------ | ----------------------------------- | ----------------------------------- | +| Retries | Polly + DelegatingHandler | Built-in `.WithRetry()` | +| Caching | Custom DelegatingHandler or nothing | Built-in `.WithCache()` | +| Cookies | Manual CookieContainer setup | Automatic `CookieJar` | +| Decompression | `AutomaticDecompression` flag | Automatic (gzip, deflate, brotli) | +| Connection pooling | SocketsHttpHandler (opaque) | Actor-based, per-host, configurable | +| Backpressure | None | End-to-end via Akka.Streams | +| Channel API | None | `ChannelWriter` / `ChannelReader` | ## What You Lose diff --git a/docs/guide/redirects.md b/docs/guide/redirects.md index 3388e7499..b1882b4b2 100644 --- a/docs/guide/redirects.md +++ b/docs/guide/redirects.md @@ -17,13 +17,13 @@ When a redirect response arrives, TurboHTTP: Each redirect status code tells TurboHTTP how to handle the follow-up request: -| Status code | What TurboHTTP does | -|-------------|---------------------| -| `301` Moved Permanently | POST becomes GET for legacy compatibility; all other methods stay the same. Body is dropped. | -| `302` Found | Same as 301 — POST becomes GET, other methods unchanged. Body is dropped. | -| `303` See Other | Always switches to GET regardless of the original method. Body is always dropped. Use this when a form submission should redirect to a results page. | -| `307` Temporary Redirect | Method and body are preserved exactly. A POST stays a POST with the same body. | -| `308` Permanent Redirect | Same as 307 — method and body are always preserved. | +| Status code | What TurboHTTP does | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `301` Moved Permanently | POST becomes GET for legacy compatibility; all other methods stay the same. Body is dropped. | +| `302` Found | Same as 301 — POST becomes GET, other methods unchanged. Body is dropped. | +| `303` See Other | Always switches to GET regardless of the original method. Body is always dropped. Use this when a form submission should redirect to a results page. | +| `307` Temporary Redirect | Method and body are preserved exactly. A POST stays a POST with the same body. | +| `308` Permanent Redirect | Same as 307 — method and body are always preserved. | **In practice:** diff --git a/docs/guide/retries.md b/docs/guide/retries.md index 30cd672d8..4fe9fce30 100644 --- a/docs/guide/retries.md +++ b/docs/guide/retries.md @@ -16,26 +16,26 @@ If all three conditions are satisfied, TurboHTTP retries the request automatical ## Method Retry Table -| Method | Retried? | Reason | -|--------|----------|--------| -| `GET` | Yes | Idempotent — reading a resource has no side effects | -| `HEAD` | Yes | Idempotent — same as GET, response body omitted | -| `PUT` | Yes | Idempotent — replacing a resource produces the same result each time | -| `DELETE` | Yes | Idempotent — deleting an already-deleted resource is still "deleted" | -| `OPTIONS` | Yes | Idempotent — capability query with no side effects | -| `TRACE` | Yes | Idempotent — diagnostic echo with no side effects | -| `POST` | **No** | Non-idempotent — sending the same POST twice could create duplicate records | -| `PATCH` | **No** | Non-idempotent — partial updates may produce different results each time | -| `CONNECT` | **No** | Non-idempotent — establishes a tunnel, not a repeatable operation | +| Method | Retried? | Reason | +| --------- | -------- | --------------------------------------------------------------------------- | +| `GET` | Yes | Idempotent — reading a resource has no side effects | +| `HEAD` | Yes | Idempotent — same as GET, response body omitted | +| `PUT` | Yes | Idempotent — replacing a resource produces the same result each time | +| `DELETE` | Yes | Idempotent — deleting an already-deleted resource is still "deleted" | +| `OPTIONS` | Yes | Idempotent — capability query with no side effects | +| `TRACE` | Yes | Idempotent — diagnostic echo with no side effects | +| `POST` | **No** | Non-idempotent — sending the same POST twice could create duplicate records | +| `PATCH` | **No** | Non-idempotent — partial updates may produce different results each time | +| `CONNECT` | **No** | Non-idempotent — establishes a tunnel, not a repeatable operation | ## Status Code Retry Table -| Status Code | Retry Behavior | Notes | -|-------------|---------------|-------| -| Network failure (no response) | Retried | Connection dropped, refused, or reset before any response arrived | -| `408 Request Timeout` | Retried | Server explicitly signals the request timed out and can be resent | -| `503 Service Unavailable` | Retried | Server temporarily unable to handle requests; respects `Retry-After` | -| Any other 4xx / 5xx | **Not retried** | Non-transient errors — retrying would not change the outcome | +| Status Code | Retry Behavior | Notes | +| ----------------------------- | --------------- | -------------------------------------------------------------------- | +| Network failure (no response) | Retried | Connection dropped, refused, or reset before any response arrived | +| `408 Request Timeout` | Retried | Server explicitly signals the request timed out and can be resent | +| `503 Service Unavailable` | Retried | Server temporarily unable to handle requests; respects `Retry-After` | +| Any other 4xx / 5xx | **Not retried** | Non-transient errors — retrying would not change the outcome | ## Retry-After Header diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index d0dd50663..6cae71405 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -55,6 +55,7 @@ Per-request overrides are also supported via `HttpRequestMessage.Version`. **Symptom:** `SendAsync` throws a timeout or connection exception. **Causes:** + 1. Server is not reachable — verify the URL and network 2. `ConnectTimeout` too low — increase it: ```csharp @@ -114,6 +115,7 @@ The built-in retry handles idempotent method detection and backoff automatically ### High Memory Usage **Possible causes:** + 1. **Cache too large** — reduce `MaxEntries` or `MaxBodyBytes` when registering: ```csharp .WithCache(c => { c.MaxEntries = 100; c.MaxBodyBytes = 10 * 1024 * 1024; }) @@ -132,6 +134,7 @@ The built-in retry handles idempotent method detection and backoff automatically **Symptom:** HTTP/2 request fails, HTTP/1.1 works. **Possible causes:** + 1. Server doesn't support HTTP/2 — use `HttpVersionPolicy.RequestVersionOrLower` to fall back 2. Cleartext HTTP/2 (h2c) not supported by server — use HTTPS 3. TLS ALPN negotiation failed — check server TLS configuration @@ -149,6 +152,7 @@ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; // gracef **Cause:** The outbound channel is full — the connection cannot send requests as fast as you produce them. This is **correct behaviour** (backpressure). **Fixes:** + 1. Use HTTP/2 for multiplexing (concurrent streams over one connection) 2. Increase `MaxConnectionsPerServer` for HTTP/1.1: ```csharp @@ -161,6 +165,7 @@ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; // gracef **Symptom:** Getting old data despite server changes. **Fixes:** + 1. Force revalidation on a specific request: ```csharp request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; @@ -191,6 +196,7 @@ akka.actor.debug.lifecycle = on ### Inspect Connection State The actor hierarchy provides connection pool visibility. Use Akka's built-in monitoring to see: + - Active connections per host - Idle connection count - Reconnect attempts diff --git a/docs/why/index.md b/docs/why/index.md index 95170f275..acea9e8b3 100644 --- a/docs/why/index.md +++ b/docs/why/index.md @@ -6,23 +6,23 @@ TurboHTTP is designed for situations where `HttpClient` alone isn't enough: high ## Feature Comparison -| Feature | HttpClient | Refit | Flurl | TurboHTTP | -|---------|:----------:|:-----:|:-----:|:---------:| -| HTTP/1.0 | ✅ | ✅ | ✅ | ✅ | -| HTTP/1.1 | ✅ | ✅ | ✅ | ✅ | -| HTTP/2 Multiplexing | ⚠️ Partial | ⚠️ Partial | ❌ | ✅ Full | -| HTTP/3 (QUIC) | ⚠️ Partial | ❌ | ❌ | ✅ Full | -| Automatic Retries | ❌ Polly needed | ❌ Polly needed | ❌ | ✅ Built-in | -| HTTP Caching | ❌ | ❌ | ❌ | ✅ Built-in | -| Cookie Management | ⚠️ Manual / CookieContainer | ⚠️ Manual | ⚠️ Manual | ✅ Automatic | -| Redirect Following | ✅ Basic | ✅ Basic | ✅ Basic | ✅ Full | -| Content Decompression | ✅ | ✅ | ✅ | ✅ | -| Connection Pooling | ✅ SocketsHttpHandler | ✅ via HttpClient | ✅ via HttpClient | ✅ Thread-safe, lock-free, per-host | -| Channel-based API | ❌ | ❌ | ❌ | ✅ | -| Backpressure | ❌ | ❌ | ❌ | ✅ Akka.Streams | -| Zero-alloc internals | ⚠️ Partial | ❌ | ❌ | ✅ Span/Memory throughout | -| Typed client interfaces | ❌ | ✅ | ❌ | ❌ | -| Fluent request builder | ❌ | ❌ | ✅ | ❌ | +| Feature | HttpClient | Refit | Flurl | TurboHTTP | +| ----------------------- | :-------------------------: | :---------------: | :---------------: | :---------------------------------: | +| HTTP/1.0 | ✅ | ✅ | ✅ | ✅ | +| HTTP/1.1 | ✅ | ✅ | ✅ | ✅ | +| HTTP/2 Multiplexing | ⚠️ Partial | ⚠️ Partial | ❌ | ✅ Full | +| HTTP/3 (QUIC) | ⚠️ Partial | ❌ | ❌ | ✅ Full | +| Automatic Retries | ❌ Polly needed | ❌ Polly needed | ❌ | ✅ Built-in | +| HTTP Caching | ❌ | ❌ | ❌ | ✅ Built-in | +| Cookie Management | ⚠️ Manual / CookieContainer | ⚠️ Manual | ⚠️ Manual | ✅ Automatic | +| Redirect Following | ✅ Basic | ✅ Basic | ✅ Basic | ✅ Full | +| Content Decompression | ✅ | ✅ | ✅ | ✅ | +| Connection Pooling | ✅ SocketsHttpHandler | ✅ via HttpClient | ✅ via HttpClient | ✅ Thread-safe, lock-free, per-host | +| Channel-based API | ❌ | ❌ | ❌ | ✅ | +| Backpressure | ❌ | ❌ | ❌ | ✅ Akka.Streams | +| Zero-alloc internals | ⚠️ Partial | ❌ | ❌ | ✅ Span/Memory throughout | +| Typed client interfaces | ❌ | ✅ | ❌ | ❌ | +| Fluent request builder | ❌ | ❌ | ✅ | ❌ | > **⚠️ Partial** means the feature exists but has constraints — for example, HttpClient's HTTP/2 support requires .NET 5+ and TLS, and its cookie support relies on a shared `CookieContainer` that requires manual setup. @@ -125,10 +125,10 @@ await foreach (var response in reader.ReadAllAsync()) ## Summary -| Scenario | Recommended | -|----------|-------------| -| Simple REST calls, small scale | `HttpClient` | -| Typed API client from interface | Refit | -| Fluent URL building | Flurl | -| High-throughput, HTTP/2 & HTTP/3, built-in caching + retry | **TurboHTTP** | -| Need Polly circuit-breaker patterns | `HttpClient` + Polly | +| Scenario | Recommended | +| ---------------------------------------------------------- | -------------------- | +| Simple REST calls, small scale | `HttpClient` | +| Typed API client from interface | Refit | +| Fluent URL building | Flurl | +| High-throughput, HTTP/2 & HTTP/3, built-in caching + retry | **TurboHTTP** | +| Need Polly circuit-breaker patterns | `HttpClient` + Polly | From ef78212321fd2cca6bdcec09be67e1412e9ac212 Mon Sep 17 00:00:00 2001 From: st0o0 <64534642+st0o0@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:08:23 +0200 Subject: [PATCH 2/2] Update docs --- docs/guide/caching.md | 11 +++-- docs/guide/cookies.md | 44 ++++++++----------- .../Architecture/Layers/16-PROTOCOL_LAYER.md | 10 ++++- .../Status/04-CURRENT_STATE_SUMMARY.md | 4 +- notes/RFC/RFC9111/RFC9111.md | 20 +++++---- 5 files changed, 48 insertions(+), 41 deletions(-) diff --git a/docs/guide/caching.md b/docs/guide/caching.md index b3d6046e8..14804b5d8 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -151,10 +151,13 @@ request.Headers.CacheControl = new CacheControlHeaderValue ## Sharing a Cache Store -By default each named client gets its own `CacheStore`. To share a single store across multiple named clients — for example, to deduplicate in-flight requests across services — create a `CacheStore` once and pass it directly: +By default each named client gets its own cache. To share a single store across multiple named clients — for example, to serve the same cached responses to parallel services — implement `ICacheStore` and pass the same instance to each client: ```csharp -var sharedStore = new CacheStore(); +using TurboHTTP.Protocol.Caching; + +// Your thread-safe ICacheStore implementation +ICacheStore sharedStore = new MySharedCacheStore(); builder.Services.AddTurboHttpClient("client-a", options => { @@ -169,6 +172,8 @@ builder.Services.AddTurboHttpClient("client-b", options => .WithCache(sharedStore); ``` -`CacheStore` is thread-safe and designed for concurrent access from multiple clients. +::: warning Thread safety +When an `ICacheStore` is shared across multiple clients it will receive concurrent reads and writes. Your implementation must be thread-safe. +::: See the [Configuration guide](./configuration) for more details on cache setup. diff --git a/docs/guide/cookies.md b/docs/guide/cookies.md index 5fd8c9a0d..e58d0f447 100644 --- a/docs/guide/cookies.md +++ b/docs/guide/cookies.md @@ -107,39 +107,31 @@ A cookie with no `Max-Age` and no `Expires` is a **session cookie** — it lives Set-Cookie: sid=abc123 ← no expiry: lasts until the client is disposed ``` -## Working with `CookieJar` Directly +## Sharing a Cookie Store -`CookieJar` is a public class. You can construct one independently to pre-populate cookies, test cookie matching logic, or share a jar across request processing outside the pipeline. +By default each named client gets its own isolated cookie store. To share cookies across multiple clients — for example, so that a login performed by one client is visible to another — implement `ICookieStore` and pass the same instance to each: ```csharp using TurboHTTP.Protocol.Cookies; -var jar = new CookieJar(); +// Your thread-safe ICookieStore implementation +ICookieStore sharedStore = new MySharedCookieStore(); -// Simulate a server response that sets a cookie -var response = new HttpResponseMessage(); -response.Headers.TryAddWithoutValidation("Set-Cookie", "session=abc123; Path=/; Secure"); -jar.ProcessResponse(new Uri("https://api.example.com"), response); +builder.Services.AddTurboHttpClient("auth", options => +{ + options.BaseAddress = new Uri("https://auth.example.com"); +}) +.WithCookies(sharedStore); -Console.WriteLine($"Cookies stored: {jar.Count}"); // → 1 - -// Inspect what would be injected into a request -var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data"); -jar.AddCookiesToRequest(new Uri("https://api.example.com/data"), ref request); -// request.Headers["Cookie"] now contains "session=abc123" - -// Clear all stored cookies (e.g., on logout) -jar.Clear(); -Console.WriteLine($"Cookies after clear: {jar.Count}"); // → 0 +builder.Services.AddTurboHttpClient("api", options => +{ + options.BaseAddress = new Uri("https://api.example.com"); +}) +.WithCookies(sharedStore); ``` -### Clearing cookies on logout - -Call `Clear()` on the jar when a user logs out to remove all stored cookies: - -```csharp -// After a successful logout response: -jar.Clear(); -``` +A cookie set during login on the `auth` client will now be available to the `api` client. -Since the per-client `CookieJar` is managed internally by the pipeline, clearing it in tests or custom integrations requires direct access to the jar instance used at construction time. +::: warning Thread safety +When an `ICookieStore` is shared across multiple clients it will receive concurrent reads and writes. Your implementation must be thread-safe. +::: diff --git a/notes/Architecture/Layers/16-PROTOCOL_LAYER.md b/notes/Architecture/Layers/16-PROTOCOL_LAYER.md index b99fd4d22..1a0c94b63 100644 --- a/notes/Architecture/Layers/16-PROTOCOL_LAYER.md +++ b/notes/Architecture/Layers/16-PROTOCOL_LAYER.md @@ -236,10 +236,18 @@ src/TurboHTTP/Protocol/ │ ├── ContentEncodingDecoder.cs │ └── … ├── Caching/ # HTTP Caching (RFC 9111) -│ ├── CacheStore.cs +│ ├── ICacheStore.cs # Store interface (custom backend extension point) +│ ├── MemoryCacheStore.cs # Default in-memory store (actor-confined) +│ ├── CacheStoreEntry.cs # Stored response snapshot (Vary, ETag, freshness) +│ ├── CacheControlStoreEntry.cs │ ├── CacheFreshnessEvaluator.cs +│ ├── CacheValidationRequestBuilder.cs +│ ├── CacheControlParser.cs │ └── … └── Cookies/ # Cookie management (RFC 6265) + ├── ICookieStore.cs # Store interface (custom backend extension point) + ├── MemoryCookieStore.cs # Default in-memory store (actor-confined) + ├── CookieStoreEntry.cs # Persisted cookie record ├── CookieJar.cs └── CookieParser.cs ``` diff --git a/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md b/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md index 5090aca9a..4924287d1 100644 --- a/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md +++ b/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md @@ -216,8 +216,8 @@ ConnectionLease ### Thread Safety - ✅ `ConnectionPool` is thread-safe (SemaphoreSlim, ConcurrentQueue) -- ✅ `CookieJar` is thread-safe (locking on writes) -- ✅ `HttpCacheStore` is thread-safe (ReaderWriterLockSlim) +- ✅ `CookieJar` is actor-confined — `MemoryCookieStore` uses a plain `List` (no locking needed) +- ✅ `MemoryCacheStore` is actor-confined — uses a plain `Dictionary` (no locking needed) - ✅ Akka stages are single-threaded per actor ### Testing diff --git a/notes/RFC/RFC9111/RFC9111.md b/notes/RFC/RFC9111/RFC9111.md index e8fac3f51..8a4759e37 100644 --- a/notes/RFC/RFC9111/RFC9111.md +++ b/notes/RFC/RFC9111/RFC9111.md @@ -16,9 +16,9 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" |--------|-------| | **Compliance Score** | 78/100 | | **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9111/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9111/` — 4 files, 75 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9111/` | +| **Implementation Path** | `TurboHTTP/Protocol/Caching/` | +| **Unit Test Files** | `TurboHTTP.Tests/Caching/` — 6 files | +| **Stream Test Files** | `TurboHTTP.StreamTests/Caching/` — 3 files | | **Key Gaps** | Shared cache support, pragma: no-cache, heuristic freshness, cache key normalization | ## Core Concepts @@ -36,10 +36,12 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" | Component | File | Purpose | |-----------|------|---------| -| `HttpCacheStore` | `Protocol/RFC9111/HttpCacheStore.cs` | Thread-safe in-memory LRU cache with Vary support | -| `CacheFreshnessEvaluator` | `Protocol/RFC9111/CacheFreshnessEvaluator.cs` | §4.2 freshness lifetime, current age, heuristic | -| `CacheValidationRequestBuilder` | `Protocol/RFC9111/CacheValidationRequestBuilder.cs` | §4.3 conditional requests, 304 merge | -| `CacheControlParser` | `Protocol/RFC9111/CacheControlParser.cs` | §5.2 Cache-Control directive parsing | +| `ICacheStore` | `Protocol/Caching/ICacheStore.cs` | Store interface — implement for a custom cache backend | +| `MemoryCacheStore` | `Protocol/Caching/MemoryCacheStore.cs` | Default in-memory store (actor-confined, no locking needed) | +| `CacheStoreEntry` | `Protocol/Caching/CacheStoreEntry.cs` | Stored response snapshot with Vary, ETag, freshness metadata | +| `CacheFreshnessEvaluator` | `Protocol/Caching/CacheFreshnessEvaluator.cs` | §4.2 freshness lifetime, current age, heuristic | +| `CacheValidationRequestBuilder` | `Protocol/Caching/CacheValidationRequestBuilder.cs` | §4.3 conditional requests, 304 merge | +| `CacheControlParser` | `Protocol/Caching/CacheControlParser.cs` | §5.2 Cache-Control directive parsing | ### Stages @@ -51,8 +53,8 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" | Test File | Coverage | |-----------|----------| -| `TurboHTTP.Tests/RFC9111/` | 75 unit tests — freshness, validation, storage, directives | -| `TurboHTTP.StreamTests/RFC9111/` | Stage behaviour tests — cache lookup and storage stages | +| `TurboHTTP.Tests/Caching/` | Unit tests — freshness, validation, storage, directives, qualified directives | +| `TurboHTTP.StreamTests/Caching/` | Stage behaviour tests — cache lookup, storage, and shared response | ## Sections