diff --git a/notes/00-Index.md b/notes/00-Index.md index 2e9f95689..877f39976 100644 --- a/notes/00-Index.md +++ b/notes/00-Index.md @@ -1,105 +1,52 @@ # TurboHTTP Knowledge Base -This is the central hub for all TurboHTTP project knowledge — connecting session logs, architecture decisions, RFC compliance notes, and feature planning. +Central hub for all TurboHTTP RFC reference knowledge. -## Architecture & Design Decisions +See [[VAULT_STYLE_GUIDE|Vault Style Guide]] for vault conventions and frontmatter standards. -- [[Architecture/00-ONBOARDING|Developer Onboarding Guide]] — Start here: project purpose, tech stack, build commands, AI & human workflows, key code patterns -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Client → Handlers → Streams → Protocol → Transport -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Port naming, conventions, stage lifecycle -- [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps & Limitations]] — Critical issues, workarounds, priority roadmap -- [[Architecture/Status/04-CURRENT_STATE_SUMMARY|Current State Summary]] — Implementation completeness, status, next milestones -- [[Architecture/Guides/05-BENCHMARK_PATTERNS|Benchmark Patterns]] — BDN conventions, port assignments, TCP TIME_WAIT workarounds -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]] — Three-layer Pipeline/EventAggregator/CompletionDecoder pattern -- [[Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION|HTTP/1.0 Reconnection Limitation]] — ExtractOptionsStage single-emit bug -- [[Architecture/Analysis/08-HTTP2_DECODER_MIGRATION|Http2Decoder Migration]] — Phases 39-62, ProtocolSession migration mapping -- [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Preferences]] — Language, knowledge capture, response style -- [[Architecture/Analysis/11-STAGE_COMPLETION_AUDIT|Stage Completion Audit]] — 48-stage audit, 20 completion propagation bugs found and fixed -- [[Architecture/Guides/12-TEST_ORGANIZATION|Test Organization]] — Test projects, base classes, fixtures, conventions, completed phases -- [[Architecture/Layers/13-CLIENT_LAYER|Client Layer]] — ITurboHttpClient, factory, DI integration, request lifecycle -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Actor-free connection pool, Channels I/O, TCP/QUIC, backpressure -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — GraphStage categories, BidiFlow composition, pipeline data flow -- [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]] — Encoder/decoder patterns, HPACK/QPACK, RFC subfolder structure -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION|Diagnostics Integration]] — DiagnosticListener, ETW EventSource, OTel Metrics +--- -### Dispatcher & Threading -- [[Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS|Dispatcher Selection Analysis]] — All six Akka.NET dispatcher types evaluated for HTTP/2 streaming -- [[Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE|Dispatcher Configuration Guide]] — ChannelExecutor configuration, tuning, and implementation steps -- [[Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE|Dispatcher Quick Reference]] — One-page decision tree and config templates -- [[Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION|ThreadPool Contention Resolution]] — ChannelExecutor migration to eliminate ThreadPool starvation +## RFC Reference Documents -### Analysis -- [[Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS|Connection Pool Hierarchy Analysis]] — Connection pool design patterns and hierarchy options -- [[Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE|Option B Implementation Guide]] — Selected connection pool architecture implementation +### HTTP Semantics & Messaging -### Guides (New) -- [[Architecture/Guides/10-TEST_CONVENTIONS|Test Conventions]] — BDD naming, Spec suffix, Trait-based RFC traceability -- [[Architecture/Guides/11-STAGE_PORT_NAMING|Stage Port Naming]] — PascalCase port naming, shape patterns, global uniqueness -- [[Architecture/Guides/12-OBSIDIAN_WORKFLOW|Obsidian Workflow]] — Vault conventions and knowledge capture workflow +| RFC | Title | Description | +|-----|-------|-------------| +| [[RFC/RFC9110/RFC9110\|RFC 9110]] | HTTP Semantics | Methods, status codes, content negotiation, conditional requests, authentication | +| [[RFC/RFC9112/RFC9112\|RFC 9112]] | HTTP/1.1 | Message framing, chunked transfer coding, persistent connections | +| [[RFC/RFC9111/RFC9111\|RFC 9111]] | HTTP Caching | Freshness, validation, Cache-Control directives, Vary-based secondary keys | +| [[RFC/RFC1945/RFC1945\|RFC 1945]] | HTTP/1.0 | Original HTTP spec — request/response format, GET/HEAD/POST, status codes | +| [[RFC/RFC6265/RFC6265\|RFC 6265]] | HTTP Cookies | Set-Cookie/Cookie headers, domain/path matching, Secure/HttpOnly/SameSite attributes | -### Benchmarks & Performance -- [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring|Benchmark 2026-04-03]] — Transport refactoring baseline -- [[Architecture/Benchmarks/Benchmark_2026-04-04_Perf_Optimizations|Benchmark 2026-04-04]] — Performance optimizations follow-up -- [[Architecture/Performance/01-BOTTLENECK_ANALYSIS_APR2026|Bottleneck Analysis (Apr 2026)]] — Systematic bottleneck analysis with profiling data -- [[Architecture/Performance/TOP_5_THROUGHPUT_OPTIMIZATIONS|Top 5 Throughput Optimizations]] — Highest-impact throughput improvements +### HTTP/2 -### HTTP/3 -- [[Architecture/Design/HTTP3_CONSOLIDATION_PLAN|HTTP/3 Consolidation Plan]] — QUIC support consolidation into stage-based architecture +| RFC | Title | Description | +|-----|-------|-------------| +| [[RFC/RFC9113/RFC9113\|RFC 9113]] | HTTP/2 | Binary framing, stream multiplexing, flow control, SETTINGS, server push | +| [[RFC/RFC7541/RFC7541\|RFC 7541]] | HPACK | Header compression for HTTP/2 — static table, dynamic table, Huffman encoding | +| [[RFC/RFC7838/RFC7838\|RFC 7838]] | Alt-Svc | HTTP Alternative Services — ALTSVC frame, Alt-Svc header, caching rules | -See [Architecture Notes](./Architecture/) for full decision records. +### HTTP/3 & QUIC -## RFC Compliance & Coverage +| RFC | Title | Description | +|-----|-------|-------------| +| [[RFC/RFC9114/RFC9114\|RFC 9114]] | HTTP/3 | QUIC-based HTTP — variable-length frames, QPACK integration, stream types | +| [[RFC/RFC9204/RFC9204\|RFC 9204]] | QPACK | Header compression for HTTP/3 — encoder/decoder streams, blocking references | +| [[RFC/RFC9000/RFC9000\|RFC 9000]] | QUIC | UDP-based multiplexed transport with built-in TLS 1.3 | -**Overall Compliance**: 86/100 — Production-Ready for HTTP/1.0, 1.1, 2.0 +--- -- [[RFC/00-RFC_STATUS_MATRIX|RFC Status Matrix]] — Detailed compliance scores, gaps, and priorities (⭐ START HERE) -- All RFC reference documents are in the [rfc/](./rfc/) folder +## RFC Dependency Map -## Features +``` +RFC 9110 (Semantics) +├── RFC 9112 (HTTP/1.1) ──────── depends on RFC 9110 +├── RFC 9111 (Caching) ───────── depends on RFC 9110 +├── RFC 9113 (HTTP/2) ────────── depends on RFC 9110 + RFC 7541 +│ └── RFC 7838 (Alt-Svc) ───── used by HTTP/2 ALTSVC frame +└── RFC 9114 (HTTP/3) ────────── depends on RFC 9110 + RFC 9204 + RFC 9000 + └── RFC 7838 (Alt-Svc) ───── used by HTTP/3 Alt-Svc header -### Protocol -- [[Features/Protocol/Feature003_Decompression_Stage|Feature 003: Decompression Stage]] — Initial standalone DecompressionStage (superseded by Feature 020) -- [[Features/Protocol/Feature004_HTTP10_Deadlock_Fix|Feature 004: HTTP/1.0 Deadlock Fix]] — Demand propagation deadlock fix via DequeueSignalStage -- [[Features/Protocol/Feature017_ConnectionStage_Race|Feature 017: ConnectionStage Race Fix]] — Race condition fixes in connection establishment -- [[Features/Protocol/Feature020_ContentEncoding_Consolidation|Feature 020: ContentEncoding Consolidation]] — Consolidation into ContentEncodingBidiStage - -### Testing -- [[Features/Testing/Feature005_H10_Flakiness_Mitigation|Feature 005: H10 Flakiness Mitigation]] — Integration test flakiness mitigation for HTTP/1.0 suite -- [[Features/Testing/Feature006_Connection_Management_Tests|Feature 006: Connection Management Tests]] — HTTP/1.1 connection management integration tests -- [[Features/Testing/Feature007_Error_Handling_Tests|Feature 007: Error Handling Tests]] — HTTP error handling and resilience integration tests -- [[Features/Testing/Feature008_TLS_Integration_Tests|Feature 008: TLS Integration Tests]] — TLS/HTTPS integration test suite -- [[Features/Testing/Feature013_Security_Tests|Feature 013: Security Tests]] — Security-focused integration tests (certificate validation, auth headers) -- [[Features/Testing/Feature014_Decoder_Fuzzing|Feature 014: Decoder Fuzzing]] — HTTP/1.x response decoder fuzz tests -- [[Features/Testing/Feature015_H2_HPACK_Fuzzing|Feature 015: H2 HPACK Fuzzing]] — HTTP/2 HPACK header compression fuzz tests - -### Diagnostics -- [[Features/Diagnostics/Feature009_Akka_Logging_Bridge|Feature 009: Akka Logging Bridge]] — Akka.NET → Microsoft.Extensions.Logging bridge -- [[Features/Diagnostics/Feature010_Tracing_Infrastructure|Feature 010: Tracing Infrastructure]] — Distributed tracing with ActivitySource and W3C trace context -- [[Features/Diagnostics/Feature011_OTel_Metrics|Feature 011: OTel Metrics]] — OpenTelemetry metrics integration -- [[Features/Diagnostics/Feature012_Diagnostic_EventSource|Feature 012: Diagnostic EventSource]] — ETW EventSource for high-performance diagnostics - -### Infrastructure -- [[Features/Infrastructure/Feature016_TracingBidi_Consolidation|Feature 016: TracingBidi Consolidation]] — Consolidation of tracing/diagnostics into TracingBidiStage -- [[Features/Infrastructure/Feature018_Docs_Site_Revision|Feature 018: Docs Site Revision]] — VitePress documentation site revision and content update -- [[Features/Infrastructure/Feature019_Stream_Survival|Feature 019: Stream Survival]] — Stream error absorption and survival hardening -- [[Features/Infrastructure/Feature025_Clean_Protocol_Core|Feature 025: Clean Protocol Core]] — Invert protocol-core topology with GroupByRequestKey routing - -### Performance -- [[Features/Performance/Feature024_Benchmark_Comparison|Feature 024: Benchmark Comparison]] — TurboHTTP vs HttpClient performance comparison - -## Active Debugging - -See [Debugging Notes](./Debugging/) for active investigations. - -## Templates - -- [[Templates/Session-Log|Session-Log]] — Daily work capture -- [[Templates/ADR|ADR]] — Architecture Decision Records -- [[Templates/RFC-Note|RFC-Note]] — RFC compliance gap tracking (distinct from RFC-Index) -- [[Templates/Bug-Investigation|Bug-Investigation]] — Structured debugging - -## Getting Started - -- [[VAULT_STYLE_GUIDE|Vault Style Guide]] — Structure, frontmatter, formatting conventions -- [[OBSIDIAN_CSS_SETUP|Obsidian CSS Setup]] — Visual consistency, theme selection, CSS snippets -- **Sessions folder**: `notes/Sessions/` — Optional session logs (use Session-Log template) +RFC 1945 (HTTP/1.0) ──────────── superseded by RFC 9112 +RFC 6265 (Cookies) ───────────── extends HTTP semantics +``` diff --git a/notes/Architecture/00-ONBOARDING.md b/notes/Architecture/00-ONBOARDING.md deleted file mode 100644 index 98a7e8d02..000000000 --- a/notes/Architecture/00-ONBOARDING.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -title: Developer Onboarding Guide -description: >- - Start here — orients new developers and fresh AI sessions to TurboHTTP - architecture, workflows, and vault navigation -tags: - - architecture - - onboarding - - guide - - meta -created: '2026-03-28' -updated: '2026-04-07' ---- -# Developer Onboarding Guide - -Welcome to TurboHTTP. This note is the single starting point for new developers and fresh AI agent sessions. Read it once, then follow the links to deeper references. - -## Project Purpose - -TurboHTTP is a high-performance HTTP client library for .NET built on Akka.Streams. It implements HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) with full RFC compliance, including: - -- Connection pooling and keep-alive management -- Redirect following and retry logic -- Cookie management and cache support -- Response decompression and request compression -- Expect-Continue handshake handling - -The library exposes an `ITurboHttpClient` interface compatible with `HttpMessageHandler`, enabling drop-in use with `HttpClient`. - -## Tech Stack - -| Component | Version | Role | -|-----------|---------|------| -| .NET | 10.0 | Target framework | -| Akka.Streams | 1.5.63 | Stream pipeline engine | -| Servus.Akka | 0.3.10 | Actor hosting utilities | -| xunit.v3 | 3.2.2 | Test framework | - -## Repository Layout - -```text -src/ -├── TurboHTTP/ # Main library -│ ├── Client/ # ITurboHttpClient, factory, DI -│ ├── Handlers/ # TurboHandler (HttpMessageHandler bridge) -│ ├── Hosting/ # DI registration extensions -│ ├── Streams/ # GraphStages: Encoding/, Decoding/, Features/, Routing/ -│ ├── Protocol/ # Encoders/Decoders, HPACK/QPACK, component subfolders -│ │ ├── Http10/ # HTTP/1.0 (RFC 1945) -│ │ ├── Http11/ # HTTP/1.1 (RFC 9112) -│ │ ├── Http2/ # HTTP/2 + Hpack/ (RFC 9113, RFC 7541) -│ │ ├── Http3/ # HTTP/3 + Qpack/ (RFC 9114, RFC 9204) -│ │ ├── Semantics/ # HTTP semantics: redirect, retry, compression (RFC 9110) -│ │ ├── Caching/ # HTTP caching (RFC 9111) -│ │ └── Cookies/ # Cookie management (RFC 6265) -│ └── Transport/ # Actor-free connection pool, Channels -│ ├── Connection/ # ConnectionPool, ConnectionLease, IConnectionScope, ConnectionStage -│ ├── Tcp/ # TcpTransportHandler, ClientState, ClientByteMover -│ └── Quic/ # QuicTransportHandler, QuicConnectionManager -├── TurboHTTP.Tests/ # Component-organized test suite -├── TurboHTTP.StreamTests/ # Akka.Streams stage tests -├── TurboHTTP.Benchmarks/ # BenchmarkDotNet performance suite -└── TurboHTTP.sln # Solution file -notes/ # This vault — single source of truth for non-code knowledge -docs/ # VitePress documentation site -``` - -## Build Commands - -```bash -# Restore and build -dotnet restore ./src/TurboHTTP.sln -dotnet build --configuration Release ./src/TurboHTTP.sln - -# Run all tests -dotnet test ./src/TurboHTTP.sln - -# Run specific test class (xUnit v3 MTP filter — note: args after --) -dotnet test ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- --filter-class "TurboHTTP.Tests.Http2.Http2DecoderBasicFrameTests" - -# Run tests for a component -dotnet test ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- --filter-namespace "TurboHTTP.Tests.Http2" - -# Run tests with specific RFC trait -dotnet test ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- --filter "Trait~RFC9113" - -# Run benchmarks -dotnet run --configuration Release ./src/TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj -``` - -### Documentation Site (requires Node.js 20+) - -```bash -cd docs && npm install -npm run docs:dev # Dev server at http://localhost:5173/TurboHTTP/ -npm run docs:build # Static site output: docs/.vitepress/dist/ -npm run docs:preview # Preview production build -``` - -## How to Navigate This Vault - -This Obsidian vault is the single source of truth for all non-code knowledge. Start with the index, then drill into relevant sections. - -**Entry points:** - -| Note | Purpose | -|------|---------| -| [[00-Index\|00-Index]] | Central hub — all categories linked from here | -| [[Architecture/Design/01-LAYERED_ARCHITECTURE\|Layered Architecture]] | Full 7-layer architecture diagram and design decisions | -| [[Architecture/Status/04-CURRENT_STATE_SUMMARY\|Current State Summary]] | Implementation completeness and next milestones | -| [[Architecture/Guides/09-CLAUDE_PREFERENCES\|Claude Preferences]] | AI session workflow, response style, knowledge capture | -| [[RFC/00-RFC_STATUS_MATRIX\|RFC Status Matrix]] | Per-RFC compliance scores and gaps (⭐ start here for RFC work) | -| [[VAULT_STYLE_GUIDE\|Vault Style Guide]] | Formatting, frontmatter, and linking conventions | - -**Architecture notes (00–17):** - -- `00` — This onboarding guide (start here) -- `01` — [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] -- `02` — GraphStage Patterns -- `03` — Known Gaps & Limitations -- `04` — Current State Summary -- `05` — Benchmark Patterns -- `06` — Decoder Pipeline Architecture -- `07` — HTTP/1.0 Reconnection Limitation -- `08` — HTTP/2 Decoder Migration -- `09` — [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Preferences]] (AI workflow) -- `11` — Stage Completion Audit -- `12` — Test Organization -- `13` — Client Layer -- `14` — Transport Layer -- `15` — Streams Layer -- `16` — Protocol Layer -- `17` — Diagnostics Integration - -## AI Agent Workflow - -### Per-Session Duties - -Every AI agent session must follow this sequence: - -1. **Orient** — Read `Architecture/Guides/09-CLAUDE_PREFERENCES` and `Architecture/Status/04-CURRENT_STATE_SUMMARY` -2. **Search before acting** — Before any RFC work: `search_notes("RFC XXXX section Y")`. Before architecture decisions: `search_notes("component name")` -3. **Work** — Implement the assigned task -4. **Capture** — Before ending the session, check: did I discover something important? If yes, write to vault - -### MCP Tools to Use - -| Task | MCP Tool | -|------|----------| -| Find existing notes | `search_notes` | -| Read a note | `read_note` | -| Create a new note | `write_note` | -| Update part of a note | `patch_note` | -| Read multiple notes at once | `read_multiple_notes` | - -**NEVER** use `Read`/`Write`/`Edit` file tools on `notes/` files — Obsidian MCP tools only. - -### Knowledge Capture Rules - -| Discovery Type | Destination | Template | -|---|---|---| -| RFC compliance gaps | `notes/RFC/` | RFC-Note | -| Architecture decisions | `notes/Architecture/` | ADR | -| Protocol limitations | `notes/Architecture/` | ADR | -| Bug investigations | `notes/Debugging/` (git-ignored) | Bug-Investigation | -| Feature learnings | `notes/Features/` | — | -| Session work logs | `notes/Sessions/` (git-ignored) | Session-Log | - -See [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Preferences]] for the full knowledge capture workflow. - -### Workflow Rules (AI) - -- **Always respond in English** — regardless of input language -- **Do NOT commit** — write `COMMIT.md` in repo root but never run `git commit` or `git add` unless explicitly asked -- **Stage files**: `git add ` only when asked, never `git add -A` -- **TreatWarningsAsErrors** is enabled globally — zero diagnostics required before any PR - -## Human Developer Workflow - -### Branching Strategy - -- `main` — stable, production-ready commits only -- Feature branches: `feature/description` or task-scoped branches -- All work happens in feature branches; merge to `main` after full verification - -### Feature Plans - -New feature work starts with a numbered feature plan. Plans include: - -- Goals and acceptance criteria with task breakdown -- Token estimates, predecessor/successor dependencies -- Model recommendations per task (haiku / sonnet / opus) - -To create a feature plan, use the `maggus:maggus-plan` skill in Claude Code. - -### PR Process - -1. Implement in a feature branch -2. Run full test suite: `dotnet test ./src/TurboHTTP.sln` -3. Verify zero diagnostics via Roslyn Navigator `get_diagnostics` -4. Stage specific changed files: `git add ` -5. Write commit message to `COMMIT.md` in repo root -6. Create PR targeting `main` - -User-visible changes are appended to the release notes after each completed task. - -## Key Code Patterns - -### GraphStage Port Naming - -All `GraphStage` inlet/outlet string names follow `StageName.Direction` or `StageName.Direction.Role` (PascalCase): - -| Shape | Inlet | Outlet | Example | -|-------|-------|--------|---------| -| FlowShape (1 in, 1 out) | `StageName.In` | `StageName.Out` | `"Http11Encoder.In"` / `"Http11Encoder.Out"` | -| FanOutShape (1 in, 2+ out) | `StageName.In` | `StageName.Out.Role` | `"Redirect.In"` / `"Redirect.Out.Final"` | -| FanInShape (2+ in, 1 out) | `StageName.In.Role` | `StageName.Out` | `"Http20Correlation.In.Request"` | -| Custom multi-port | `StageName.In.Role` | `StageName.Out.Role` | `"Http20Connection.In.Server"` | - -Rules: PascalCase, no protocol prefix, no `Stage` suffix, semantic role names (`Request`, `Response`, `Final`, `Retry`, `Signal`, `Hit`, `Miss`, `Server`, `Stream`, `App`), globally unique names across the solution. - -### C# Style - -```csharp -// Allman braces — opening brace on new line -public sealed class MyStage -{ - private readonly string _fieldName; - - public void DoSomething() - { - // Always use braces for control structures (even single-line) - if (condition) - { - DoThing(); - } - } -} -``` - -Key rules: - -- Allman style braces (opening brace on new line) -- 4 spaces indentation, no tabs -- Private fields prefixed with underscore `_fieldName` -- Use `var` when type is apparent -- Default to `sealed` classes and records -- Do NOT add `#nullable enable` — enabled at project level in `.csproj` -- Never use `async void`, `.Result`, or `.Wait()` -- Always pass `CancellationToken` through async call chains -- Always use braces for control structures, even single-line - -### Test Conventions (Post-Feature-040) - -```csharp -// Namespace matches component folder -namespace TurboHTTP.Tests.Http2.Encoding; - -public sealed class Http2EncoderSpec : StreamTestBase -{ - // Timeout is REQUIRED on all async tests - [Fact(Timeout = 5000)] - [Trait("RFC", "RFC9113-4.1")] - public async Task Http2Encoder_should_encode_data_frame_correctly() - { - // ... - } - - // Theory with InlineData for parameterised cases - [Theory(Timeout = 5000)] - [InlineData("GET"), InlineData("POST")] - [Trait("RFC", "RFC9113-4.3")] - public async Task Http2Encoder_must_include_method_pseudo_header(string method) - { - // ... - } -} -``` - -Key rules (post-Feature-040): - -- **Test classes**: `public sealed class`, namespace matches component folder (e.g., `TurboHTTP.Tests.Http2.Encoding`, `TurboHTTP.Tests.Caching`) -- **File naming**: `Spec.cs` — descriptive name with `Spec` suffix (Akka.NET convention) -- **Use `[Fact]`** for single cases, **`[Theory]`** + **`[InlineData]`** for parameterised cases -- **RFC Traceability**: `[Trait("RFC", "RFC-
")]` (e.g., `[Trait("RFC", "RFC9113-4.1")]`) - - CI filter: `dotnet test --filter "Trait~RFC9113"` -- **Method names**: BDD style `Subject_should_behavior()` (e.g., `Http2Encoder_should_encode_data_frame_correctly()`) -- **Timeout is REQUIRED** — all async tests must have `[Fact(Timeout = 5000)]` or `[Theory(Timeout = 5000)]` -- **Max 500 lines per test class** — split into multiple files if exceeded -- Do NOT add `#nullable enable` at the top of test files - -## See Also - -- [[VAULT_STYLE_GUIDE|Vault Style Guide]] — how to write notes, frontmatter standards, quality checklist -- [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Preferences]] — AI session workflow in detail -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — full architecture reference -- [[Architecture/Status/04-CURRENT_STATE_SUMMARY|Current State Summary]] — project status and roadmap -- [[RFC/00-RFC_STATUS_MATRIX|RFC Status Matrix]] — compliance tracking by RFC -- [[Architecture/Guides/12-TEST_ORGANIZATION|Test Organization]] — detailed test folder mapping and structure diff --git a/notes/Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION.md b/notes/Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION.md deleted file mode 100644 index 04d97e060..000000000 --- a/notes/Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: HTTP/1.0 Pipeline Reconnection Limitation -description: >- - ExtractOptionsStage emits ConnectItem once per client — HTTP/1.0 - redirect/retry cannot reconnect after connection-close -tags: - - architecture - - http10 - - pipeline - - resolved -aliases: - - HTTP/1.0 Reconnection Bug - - ExtractOptionsStage Limitation -status: resolved ---- -# HTTP/1.0 Pipeline Reconnection Limitation - -**Discovered**: 2026-03-23 during HTTP/1.0 redirect integration testing -**Status**: ✅ Resolved (Feature 030, TASK-030-006 + TASK-030-007) - -## Problem (Historical) - -HTTP/1.0 redirect and retry integration tests were **BLOCKED** because the Akka.Streams pipeline could not reconnect after HTTP/1.0 connection-close. - -## Root Cause - -`ExtractOptionsStage` emitted a `ConnectItem` only once (via `_initialSent` flag). When HTTP/1.0 closed the connection after each response, follow-up requests had no `ConnectItem` — so `ConnectionStage` had no handle and dropped data. - -## Resolution - -Resolved by the IConnectionScope architecture (Feature 030): - -1. **ConnectionStage** now takes `IConnectionScope` instead of `ConnectionPool` — auto-reconnects when `DataItem` arrives with `_handle == null` via `scope.AcquireAsync()` -2. **ConnectionReuseFlowStage** replaced `ConnectionReuseStage` — calls `scope.ReturnAsync(canReuse)` which triggers transport callback for cleanup -3. **ExtractOptionsStage simplified** — `InReuse` inlet removed, `_needsReconnect` field removed, feedback loop eliminated -4. **Linear topology** — `BuildConnectionFlow()` is cycle-free, no `Broadcast(eagerCancel)` needed - -The entire feedback loop (Broadcast + 2× MergePreferred + ExtractOptionsStage.InReuse) has been eliminated. Per-host `IConnectionScope` instances (`SingleRequestConnectionScope` for HTTP/1.0, `PersistentConnectionScope` for HTTP/1.1+) mediate connection lifecycle through method calls, not graph edges. - -## See Also - -- [[Architecture/Analysis/10-DEADLOCK_ANALYSIS|Deadlock Analysis Catalog]] — DL-006, DL-009, DL-010 all marked Fixed diff --git a/notes/Architecture/Analysis/08-HTTP2_DECODER_MIGRATION.md b/notes/Architecture/Analysis/08-HTTP2_DECODER_MIGRATION.md deleted file mode 100644 index e39f39d94..000000000 --- a/notes/Architecture/Analysis/08-HTTP2_DECODER_MIGRATION.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Http2Decoder Migration Plan -description: >- - Migration from monolithic Http2Decoder to stage-based testing via - Http2ProtocolSession and Http2StageTestHelper (Phases 39-62) -tags: - - architecture - - refactoring - - http2 - - testing - - migration -aliases: - - Http2Decoder Removal - - Stage Testing Migration ---- -# Http2Decoder Migration Plan - -**Last Updated**: 2026-03-26 -**Plan File**: `IMPLEMENTATION_PLAN.md` (repo root) - -## Problem - -- `Http2Decoder` (55KB): monolithic test helper, NOT used in production -- 500+ test references create maintenance debt -- Gap between test code (Http2Decoder) and production code (Stages) -- RFC compliance: 185 HTTP/2 tests need architecture improvement - -## Phase Status - -### Phases 39-43: Stage Testing Foundation - -| Phase | Description | Status | Effort | -|-------|-------------|--------|--------| -| 39 | Deprecate Http2Decoder, organize files | ✅ COMPLETE | 2-3h | -| 40 | Create Http2StageTestHelper framework | Ready | 8-10h | -| 41 | Migrate RFC9113 sections 1-5 (73 tests) | Ready | 12-16h | -| 42 | Migrate RFC9113 sections 6-9 (78 tests) | Ready | 12-16h | -| 43 | Validation gate + regression testing | Ready | 4-6h | - -**Phase 39 commits**: `85309d5` & `6586949` - -### Phases 44-62: Http2Decoder Removal - -**Goal**: Remove `Http2Decoder`, `Http2DecodeResult`, `Http2StreamLifecycleState` from production - -**Key**: Phase 44 creates `Http2ProtocolSession` (test helper in `src/TurboHTTP.Tests/Http2ProtocolSession.cs`) — lightweight stateful wrapper over `Http2FrameDecoder`. - -### Migration Mapping - -| Old (Http2Decoder) | New (Http2ProtocolSession) | -|---------------------|----------------------------| -| `new Http2Decoder()` | `new Http2ProtocolSession()` | -| `TryDecode(bytes, out _)` | `session.Process(bytes)` | -| `GetStreamLifecycleState(id)` | `session.GetStreamState(id)` | -| `GetActiveStreamCount()` | `session.ActiveStreamCount` | -| `GetMaxConcurrentStreams()` | `session.MaxConcurrentStreams` | -| `IsGoingAway` / `GetGoAwayLastStreamId()` | `session.IsGoingAway` / `session.GoAwayLastStreamId` | -| `result.Responses` | `session.Responses` | -| `Reset()` | `new Http2ProtocolSession()` | -| `ValidateServerPreface()` | `Http2StageTestHelper.ValidateServerPreface()` | - -**Scope**: 22 files, ~428 Http2Decoder references diff --git a/notes/Architecture/Analysis/10-DEADLOCK_ANALYSIS.md b/notes/Architecture/Analysis/10-DEADLOCK_ANALYSIS.md deleted file mode 100644 index 4fafa9776..000000000 --- a/notes/Architecture/Analysis/10-DEADLOCK_ANALYSIS.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -title: Deadlock Analysis Catalog -created: '2026-03-26' -tags: - - architecture - - deadlock - - catalog -status: all-fixed ---- -# Deadlock Analysis Catalog - -Complete catalog of all known deadlock patterns in TurboHTTP, organized by layer. Each entry includes root cause, affected files, fix status, and test coverage. - -> **DL-009** and **DL-010** are **Fixed** — resolved by Feature 030 (IConnectionScope + linear topology rewrite). - ---- - -## Async-Boundary Diagram - -The core pipeline topology where most deadlocks occur: - -``` -ChannelSource - │ - ▼ -KillSwitch - │ - ▼ -┌─────────────────────────┐ -│ RetryBidi │ CacheBidi │ ◄── Feature BidiStages (feedback re-injection) -└─────────────────────────┘ - │ - ▼ -GroupByHostKey ─────────────── [Source.Queue Boundary] ◄── fusion island break - │ - ▼ -Substream (per host-key) - │ - ▼ -ExtractOptions → Encoder → ConnectionStage → Decoder → ConnectionReuse - │ - ▼ -MergeSubstreams - │ - ▼ -Response outlet -``` - -**Key boundary**: `Source.Queue` inside `GroupByHostKeyStage` creates an async boundary between the feature BidiStage stack and per-host substreams. Callbacks from substream completion (`WatchTask`) run on different execution contexts than `onUpstreamFinish` in the BidiStages above. - ---- - -## Deadlock Catalog - -### Summary Table - -| ID | Name | Category | Status | -|--------|----------------------------------------------|------------------------|-----------------| -| DL-001 | GroupByHostKey Two-Phase Completion | Akka.Streams Internal | Fixed | -| DL-002 | OfferAsync Timeout Race | Akka.Streams Internal | Fixed | -| DL-003 | Unknown Encoding Pass-Through | Akka.Streams Internal | Fixed | -| DL-004 | ConnectionStage Generation Guard | Akka.Streams Internal | Fixed | -| DL-005 | ConnectionReuse Signal Ordering | Akka.Streams Internal | Fixed | -| DL-006 | ExtractOptions Reconnection Window | HTTP/1.0 Pipeline | Fixed | -| DL-007 | MergeSubstreams Zombie Prevention | Akka.Streams Internal | Fixed | -| DL-008 | Feedback Buffer Backpressure | Akka.Streams Internal | Fixed | -| DL-009 | RetryBidi _inFlightCount Race | HTTP/1.0 Reconnect | Fixed | -| DL-010 | CacheBidi ReadAsByteArrayAsync Blocking | HTTP/1.0 Reconnect | Fixed | -| DL-011 | Materializer Buffer Sizing | Akka.Streams Internal | Fixed | -| DL-012 | ConnectionPool Semaphore Starvation | Transport Layer | Design Pattern | -| DL-013 | ClientState Channel Direction | Transport Layer | Design Pattern | - ---- - -### DL-001: GroupByHostKey Two-Phase Completion - -- **Category**: Akka.Streams Internal — Completion Race -- **Status**: Fixed -- **Root Cause**: `CompleteStage()` called immediately after substream queue completion, but downstream BidiStages (RetryBidiStage, CacheBidiStage) still hold re-injection requests. Outlet becomes dead before retry/cache can push back, causing silent hang. -- **Affected Files**: `Streams/Stages/Routing/GroupByHostKeyStage.cs` -- **Fix**: Implemented two-phase completion with `TryCompleteStage()` that defers stage completion until all substream `WatchTask`s report `IsDead == true`. Callbacks on WatchTask completion trigger final `CompleteStage()`. -- **Test IDs**: FBUF-001 through FBUF-006, Deadlock-H10-001, Reinjection-H10-001 - -### DL-002: OfferAsync Timeout Race - -- **Category**: Akka.Streams Internal — Ask Pattern Timeout -- **Status**: Fixed -- **Root Cause**: `Source.Queue.OfferAsync()` internally uses Ask pattern with 5-second timeout. Between `IsDead` check and `OfferAsync` call, queue actor dies — waits for full timeout instead of detecting immediately. -- **Affected Files**: `Streams/Stages/Routing/GroupByHostKeyStage.cs` (line ~325-334) -- **Fix**: Race `offerTask` against `state.WatchTask` using `Task.WhenAny()`. When queue dies, WatchTask completes first, giving sub-millisecond detection instead of 5-second timeout. -- **Test IDs**: DLAK-002, Reinjection-H10-001, Reinjection-H10-002 - -### DL-003: Unknown Encoding Pass-Through - -- **Category**: Akka.Streams Internal — Encoding Failure -- **Status**: Fixed -- **Root Cause**: Pipeline would deadlock if server returned unknown compression encoding. `ContentEncodingBidiStage` threw unhandled `HttpDecoderException` which killed the stage without propagating completion signals downstream. -- **Affected Files**: `Streams/Stages/Features/ContentEncodingBidiStage.cs` -- **Fix**: Catch `HttpDecoderException` and pass response through unchanged when encoding is unrecognized. Unknown encodings no longer kill the pipeline. -- **Test IDs**: Deadlock-H10-003 - -### DL-004: ConnectionStage Stale Callback Race (Generation Guard) - -- **Category**: Akka.Streams Internal — Async Callback Race -- **Status**: Fixed -- **Root Cause**: After HTTP/1.0 connection close, inbound pump drains asynchronously. Stale async callbacks from the old pump could inject `CloseSignalItem` into the new connection's decoder via `GetAsyncCallback`, corrupting state. -- **Affected Files**: `Transport/ConnectionStage.cs` -- **Fix**: Introduced `_connectionGen` (generation counter) that increments on reconnect. Stale callbacks check generation before posting — generation mismatch means callback is ignored. -- **Test IDs**: CS-RC-001 - -### DL-005: ConnectionReuse Signal Ordering - -- **Category**: Akka.Streams Internal — Outlet Ordering -- **Status**: Fixed -- **Root Cause**: If response outlet pushed before signal outlet, the redirect/retry feedback path was not yet set. Follow-up requests skip `ConnectItem` emission, causing connection setup to stall. -- **Affected Files**: `Streams/Stages/Features/ConnectionReuseStage.cs` (line ~61-67) -- **Fix**: Always `TryPushSignal()` before `TryPushResponse()`. Signal sets `_needsReconnect` flag in ExtractOptionsStage before response pushes through the fused graph. -- **Test IDs**: DLH10-004 - -### DL-006: ExtractOptions Reconnection Window - -- **Category**: HTTP/1.0 Pipeline -- **Status**: Fixed -- **Root Cause**: `ExtractOptionsStage` emits `ConnectItem` only once via `_initialSent` flag. After HTTP/1.0 connection close, retry/redirect recirculated requests flow through encoder without a new `ConnectItem` — `ConnectionStage` has no handle to establish a new connection. -- **Affected Files**: `Streams/Stages/Routing/ExtractOptionsStage.cs` -- **Fix**: Eliminated entirely by Feature 030 architecture rewrite. `ConnectionStage` now uses `IConnectionScope` for auto-reconnect — when a `DataItem` arrives with `_handle == null`, it acquires a new connection via `scope.AcquireAsync()` using stored options. No `ConnectItem` feedback loop needed. `ExtractOptionsStage.InReuse` inlet removed; `_needsReconnect` field removed. -- **Test IDs**: DLH10-005, Reinjection-H10-001 through Reinjection-H10-003 -- **See also**: [[07-HTTP10_RECONNECTION_LIMITATION]] - -### DL-007: MergeSubstreams Zombie Prevention - -- **Category**: Akka.Streams Internal — Substream Completion -- **Status**: Fixed -- **Root Cause**: If upstream finishes before all active substreams complete, zombie substream actors linger after materializer shutdown. `onUpstreamFailure` did not set `_upstreamDone`, so substream callbacks waited indefinitely. -- **Affected Files**: `Streams/Stages/Routing/MergeSubstreamsStage.cs` (line ~72-94) -- **Fix**: On `onUpstreamFailure`, set `_upstreamDone = true` so substream callbacks recognize terminal state and trigger `CompleteStage()` without hanging. -- **Test IDs**: DLAK-004, SURV-006 through SURV-008 - -### DL-008: Feedback Buffer Backpressure - -- **Category**: Akka.Streams Internal — Backpressure Stall -- **Status**: Fixed -- **Root Cause**: Feedback path (redirect/retry re-injection) blocked when downstream backpressure prevented the response outlet from draining. Requests piled up in the feedback loop with no way to make progress. -- **Affected Files**: `Streams/ProtocolCoreGraphBuilder.cs` (feedback path wiring) -- **Fix**: Buffer feedback path with configurable capacity to decouple response consumption from re-injection request generation. -- **Test IDs**: FBUF-001 through FBUF-006 - -### DL-009: RetryBidi _inFlightCount Race - -- **Category**: HTTP/1.0 Reconnect — In-Flight Request Window -- **Status**: Fixed (Feature 030, TASK-030-001) -- **Root Cause**: Window between `_inFlightCount` decrement (response received) and retry enqueue (decision to retry). If `TryCompleteIfDone()` fires in this window, it sees zero in-flight requests and closes the outlet prematurely. The retry request has nowhere to go. -- **Affected Files**: `Streams/Stages/Features/RetryBidiStage.cs` -- **Fix**: Added `_retryTransactionActive` boolean field as atomic transaction guard. Set `true` before retry evaluation, `false` after `_inFlightCount--` and `TryPullResponse()`. `TryCompleteIfDone()` returns early if transaction is active. Same pattern applied to `OnTimer()` delayed retry path. -- **Test IDs**: DLH10-001 -- **Symptom** (before fix): Pipeline hangs ~10-15 seconds after 503 response when retry is attempted on HTTP/1.0 connection. - -### DL-010: CacheBidi ReadAsByteArrayAsync Blocking - -- **Category**: HTTP/1.0 Reconnect — Async Body Read -- **Status**: Fixed (Feature 030, TASK-030-003) -- **Root Cause**: `ReadAsByteArrayAsync()` holds the stage actor scope while the async body read runs on the thread pool. While the stage is blocked, `GroupByHostKeyStage` sees the substream queue as idle and calls `CompleteStage()` prematurely. The cache stage then waits for demand that never comes (Out1 is already cancelled). -- **Affected Files**: `Streams/Stages/Features/CacheBidiStage.cs` -- **Fix**: Added `_pendingAsyncRead` flag as backpressure guard. All `TryPull*` methods check `if (_pendingAsyncRead) return;` to prevent inlet pulls during async body reads. After async callback fires and `_pendingAsyncRead = false`, pulling resumes. GroupByHostKeyStage liveness guard (TASK-030-002) also defers completion while substreams are alive but idle. -- **Test IDs**: DLH10-002 -- **Symptom** (before fix): Pipeline hangs ~10-15 seconds when CacheBidiStage attempts to cache response body from HTTP/1.0 connection. - -### DL-011: Materializer Buffer Sizing - -- **Category**: Akka.Streams Internal — Buffer Configuration -- **Status**: Fixed -- **Root Cause**: Default materializer buffer (16/16) insufficient for pipelined feedback loops. Responses arrive faster than they are consumed when redirect/retry chains are active, causing the entire pipeline to stall under backpressure. -- **Affected Files**: `Streams/TurboClientStreamManager.cs` (materializer configuration) -- **Fix**: Applied custom `ActorMaterializerSettings` with tuned `InputBuffer` sizing to prevent tight-loop backpressure stalls in feedback paths. -- **Test IDs**: MBUF-001 through MBUF-006 - -### DL-012: ConnectionPool Semaphore Starvation - -- **Category**: Transport Layer — Semaphore Management -- **Status**: Design Pattern (preventive) -- **Root Cause**: If connection lease disposal fails to release semaphore on abrupt close, other `AcquireAsync` callers wait indefinitely on `SemaphoreSlim.WaitAsync()`. All connection slots become permanently consumed. -- **Affected Files**: `Transport/ConnectionPool.cs` (HostConnections, line ~92-112) -- **Fix**: `ConnectionLease.Dispose()` always calls `Release()` on semaphore via `finally` block, even on exception. `isAbruptClose` flag ensures cleanup path runs regardless of how the connection terminated. -- **Test IDs**: DLTP-001, DLTP-002 - -### DL-013: ClientState Channel Direction - -- **Category**: Transport Layer — Channel State Machine -- **Status**: Design Pattern (preventive) -- **Root Cause**: If read pump exits but write pump tries to read from a pre-completed channel (or vice versa), the write pump hangs indefinitely waiting for data that will never arrive. -- **Affected Files**: `Transport/ClientState.cs` (line ~41-70, 89-102) -- **Fix**: `ClientState` constructor accepts `StreamDirection` enum (`ReadOnly`, `WriteOnly`, `Bidirectional`). Pre-completes unused channels so unused pumps exit immediately without blocking. -- **Test IDs**: DLTP-005 - ---- - -## Categories - -### Akka.Streams Internal (DL-001 through DL-005, DL-007, DL-008, DL-011) -Completion races, async callback timing, buffer sizing, and outlet ordering within the Akka.Streams fusion framework. Most are caused by the `Source.Queue` async boundary inside `GroupByHostKeyStage`. - -### HTTP/1.0 Reconnect (DL-006, DL-009, DL-010) -Deadlocks specific to HTTP/1.0 connection-close semantics where the TCP connection must be re-established for retry/redirect/cache operations. The connection close propagates through stages faster than the feature BidiStages can react. - -### Transport Layer (DL-012, DL-013) -Preventive design patterns in the connection pool and byte mover to avoid semaphore starvation and channel state machine deadlocks. - ---- - -## Status Legend - -| Status | Meaning | -|-----------------|---------| -| **Fixed** | Root cause identified and fix implemented with regression tests | -| **Known Limitation** | Understood behavior with documented workaround, not yet fully resolved | -| **Active Bug** | Confirmed bug, tests written as Skip, fix pending in separate task | -| **Design Pattern** | Preventive pattern built into the architecture to avoid the deadlock class | diff --git a/notes/Architecture/Analysis/11-STAGE_COMPLETION_AUDIT.md b/notes/Architecture/Analysis/11-STAGE_COMPLETION_AUDIT.md deleted file mode 100644 index 3d1babfcb..000000000 --- a/notes/Architecture/Analysis/11-STAGE_COMPLETION_AUDIT.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: Stage Completion Propagation Audit -description: >- - Systematic audit of 48 GraphStage implementations finding 20 completion - propagation bugs — all fixed -tags: - - architecture - - stages - - audit - - reactive-streams ---- -# Stage Completion Propagation Audit - -## Executive Summary - -A systematic audit of all 48 GraphStage implementations in TurboHTTP found **20 confirmed bugs** where stream termination signals (onUpstreamFinish, onUpstreamFailure, onDownstreamFinish) were not properly propagated. These omissions violated the Reactive Streams contract and could lead to **backpressure deadlocks**, where downstream stages wait indefinitely for termination signals that never arrive. - -**Status (2026-03-27): All 20 bugs fixed.** Each fix adds `FailStage(ex)` (or `Fail(outlet, ex)` for BidiStages) after existing logging. 17 regression tests added in `TurboHTTP.StreamTests/Streams/26–29_*StageCompletionRegressionTests.cs`. **0 open bugs remain.** - ---- - -## Can Missing Completion Handlers Cause Deadlocks? - -**YES. According to Akka.Streams documentation:** - -When a stage's `onUpstreamFailure` handler is overridden with only logging and no call to `CompleteStage()` or `FailStage()`, the **default completion propagation is suppressed**. This means: - -1. **Downstream remains in demand state forever** — it calls `Pull()` expecting an element or completion signal, but neither arrives. -2. **Backpressure stall** — the downstream actor is suspended waiting for upstream to respond; no CPU work progresses. -3. **Resource leak** — in HTTP/2 and HTTP/3 pipelines, the TCP/QUIC write pump stalls. Connection resources are not released, and actor mailboxes accumulate. -4. **Akka.Streams contract violation** — per Reactive Streams spec, a stage must eventually inform downstream that no more elements will arrive. - -For HTTP request pipelines specifically: -- If `Http20EncoderStage._in` absorbs a network failure without closing `_out`, the framing stage downstream keeps waiting for frames. -- The HTTP/2 stream multiplexing layer waits for the encoder to emit the next frame — this wait is **indefinite if the encoder's outlet never completes**. -- Connection pooling clients will see the request hang; connection reuse is blocked. - ---- - -## Bug Pattern: Named-Parameter `onUpstreamFailure` Only Logging - -### Root Cause - -```csharp -// BUGGY form (in 20 stages): -SetHandler(inlet, - onPush: () => {...}, - onUpstreamFinish: () => {...}, - onUpstreamFailure: ex => Log.Warning("... {0}", ex.Message)); // ← Explicit handler -``` - -When you explicitly set `onUpstreamFailure` with only a log statement and **no** `CompleteStage()` or `FailStage()`, you **override Akka's default**. The default is `FailStage(ex)`, which closes all ports. By overriding it with only logging, you suppress that default. Result: outlet stays **permanently open**. - -### Why the Default Exists - -Akka.Streams' default `onUpstreamFailure: FailStage(ex)` immediately propagates the upstream failure to all outlets. This is the correct behavior per Reactive Streams: when upstream fails, downstream must be notified so it can release resources and exit its demand loop. - -### Correct Alternatives - -**Option 1 — Absorb explicitly**: -```csharp -SetHandler(inlet, - onPush: () => {...}, - onUpstreamFinish: () => Complete(outlet), - onUpstreamFailure: ex => - { - Log.Warning("... {0}", ex.Message); - Complete(outlet); - }); -``` - -**Option 2 — Use single-action form (uses defaults)**: -```csharp -SetHandler(inlet, () => Push(outlet, Grab(inlet))); -// Defaults: onUpstreamFinish = CompleteStage, onUpstreamFailure = FailStage -``` - ---- - -## Confirmed Bugs: 20 Instances - -### Critical — Outlet Permanently Open - -| ID | Stage | File | Issue | Impact | Status | -|----|-------|------|-------|--------|--------| -| B-001 | TracingBidiStage | Features/TracingBidiStage.cs | `_inResponse.onUpstreamFailure` logs but **missing** `Complete(_outResponse)` | Response path stalls on network error | **Fixed** | -| B-002 | Http20DecoderStage | Decoding/Http20DecoderStage.cs | `_in.onUpstreamFailure` only logs → `_out` open | Downstream waiting for stream termination | **Fixed** | -| B-003 | Http20StreamIdAllocatorStage | Routing/Http20StreamIdAllocatorStage.cs | `_in.onUpstreamFailure` only logs | Stream ID allocation blocked | **Fixed** | -| B-004 | Http20CorrelationStage | Routing/Http20CorrelationStage.cs | **Both** `_inRequest.onUpstreamFailure` and `_inResponse.onUpstreamFailure` only log | Bidirectional stall | **Fixed** | -| B-005 | Http20ConnectionStage | Decoding/Http20ConnectionStage.cs | `_inApp.onUpstreamFailure` missing (unregistered) | Intentional? Design concern | **Fixed** | -| B-008 | Http20PrependPrefaceStage | Encoding/Http20PrependPrefaceStage.cs | `_in.onUpstreamFailure` only logs | Preface stream stalls | **Fixed** | -| B-009 | Http20Request2FrameStage | Encoding/Http20Request2FrameStage.cs | `_in.onUpstreamFailure` only logs | Frame encoding blocked | **Fixed** | -| B-010 | Http30ConnectionStage | Decoding/Http30ConnectionStage.cs | `_inApp.onUpstreamFailure` missing | Design concern | **Fixed** | -| B-011 | Http30DecoderStage | Decoding/Http30DecoderStage.cs | `_in.onUpstreamFailure` only logs | Downstream waiting | **Fixed** | -| B-012 | Http30StreamStage | Decoding/Http30StreamStage.cs | `_in` has `onUpstreamFinish` but **no** `onUpstreamFailure` | Failure case unhandled | **Fixed** | -| B-014 | Http30ControlStreamPrefaceStage | Encoding/Http30ControlStreamPrefaceStage.cs | `_in.onUpstreamFailure` only logs | QUIC control stream stalls | **Fixed** | -| B-015 | Http30QpackEncoderPrefaceStage | Encoding/Http30QpackEncoderPrefaceStage.cs | `_in.onUpstreamFailure` only logs | QPACK encoder stalls | **Fixed** | -| B-016 | Http30Request2FrameStage | Encoding/Http30Request2FrameStage.cs | `_in.onUpstreamFailure` only logs | QUIC frame encoding blocked | **Fixed** | -| B-017 | Http30CorrelationStage | Routing/Http30CorrelationStage.cs | **Both** `_inRequest.onUpstreamFailure` and `_inResponse.onUpstreamFailure` only log | Bidirectional stall | **Fixed** | -| B-018 | Http30StreamDemuxStage | Routing/Http30StreamDemuxStage.cs | `_in.onUpstreamFailure` only logs | Stream demux blocked | **Fixed** | -| B-019 | QpackDecoderStreamStage | Decoding/QpackDecoderStreamStage.cs | `_in.onUpstreamFailure` only logs | QPACK decoder stalls | **Fixed** | -| B-020 | QpackEncoderStreamStage | Encoding/QpackEncoderStreamStage.cs | `_in.onUpstreamFailure` only logs | QPACK encoder stalls | **Fixed** | - -### Design Concern — Single-Action Form, Default `FailStage` May Be Too Aggressive - -| ID | Stage | File | Issue | Concern | -|----|-------|------|-------|---------| -| B-006 | Http20StreamStage | Decoding/Http20StreamStage.cs | Both ports use `SetHandler(inlet, () => ...)` — single-action form, defaults apply | Default `FailStage(ex)` immediately fails **all** outlets. For HTTP/2 streams, a single stream error should not fail connection-level streams. | -| B-007 | Http20EncoderStage | Encoding/Http20EncoderStage.cs | Same as B-006 | Same concern | -| B-013 | Http30EncoderStage | Encoding/Http30EncoderStage.cs | Same as B-006 | Same concern | - -### Intentional Design (Not Bugs) - -| Stage | File | Pattern | -|-------|------|---------| -| Http20ConnectionStage | Decoding/Http20ConnectionStage.cs | `_inApp.onUpstreamFinish: () => {}` — intentionally empty to keep request outlet alive for pending responses | -| Http30ConnectionStage | Decoding/Http30ConnectionStage.cs | Same pattern | - ---- - -## Clean Stages: 19 Instances - -All ports properly handle termination signals: -- **Feature BidiStages** (4): RetryBidiStage, RedirectBidiStage, CacheBidiStage, CookieBidiStage, HandlerBidiStage, ContentEncodingBidiStage, ExpectContinueBidiStage -- **HTTP/1.x stages** (6): Http10DecoderStage, Http10EncoderStage, Http11DecoderStage, Http11EncoderStage, Http1XCorrelationStage, RequestEnricherStage -- **Routing & Multiplexing** (3): ConnectionReuseStage, GroupByHostKeyStage, MergeSubstreamsStage, ExtractOptionsStage -- **QPACK feedback sink** (1): QpackDecoderFeedbackStage -- **Diagnostics** (1): DeadlockWatchdogStage - ---- - -## Impact Assessment - -### HTTP/2 and HTTP/3 Encoding Pipeline - -**Scenario**: Client sends a request while the server closes the connection (sends GOAWAY). - -1. Encoder reads the request. -2. TCP/connection failure propagates as exception upstream to `Http20/30EncoderStage._in`. -3. Encoder's `onUpstreamFailure` **only logs**, does not call `CompleteStage()`. -4. Encoder's `_out` remains open. -5. Downstream `Http20/30PrependPrefaceStage` calls `Pull()` for the next frame — **waits forever**. -6. Preface stage is now suspended in demand. -7. The HTTP/1.x layer / connection pooling client perceives a **hang**. - -### GroupByHostKeyStage + Feature BidiStages - -**Scenario**: HTTP/2 stream receives a 503 error; RetryBidiStage re-injects a retry. - -1. Response arrives on `Http20DecoderStage`, which absorbs upstream failures silently. -2. If the downstream transport fails during response delivery, the decoder's outlet never closes. -3. RetryBidiStage waits for the response to complete so it can decrement `_inFlightCount`. -4. GroupByHostKeyStage's `TryCompleteIfDone()` waits for all in-flight responses to resolve. -5. **Deadlock**: all three stages suspended in cross-wait. - ---- - -## Reactive Streams Contract Violation - -Per RFC 7231 (Reactive Streams) and Akka.Streams documentation: - -> **Section 2.1** — Demand is fulfilled by Subscription passing values to its Subscriber. The first is to send up to n elements on each event. -> **Section 2.7** — If the upstream fails during [element delivery], the Subscription **MUST call onError on the Subscriber** and the Subscriber is expected to **release all resources**. - -When a stage fails to call `Complete(outlet)` or `FailStage(ex)`, it violates Section 2.7. Downstream cannot release resources or detect that upstream will no longer produce elements. - ---- - -## Recommendations - -### Short-term (Immediate Fix) - -For each of the 20 buggy stages: -1. Add `CompleteStage()` or `Complete(outlet)` to **all** `onUpstreamFailure` handlers. -2. For BidiStages, ensure both request and response directions are explicitly closed. - -### Medium-term (H2/H3 Stream Error Handling) - -Stages B-006, B-007, B-013 (single-action form with default `FailStage`): -- Replace with **explicit named-parameter handlers** that call `Complete(outlet)` instead of allowing `FailStage(ex)` to propagate to all outlets. -- This prevents a single-stream error from failing the entire connection. - -### Long-term (Testing) - -- Add `stage-completion-verification` test that validates every `GraphStage` has explicit handlers on all ports. -- Test failure scenarios: upstream exception, downstream cancellation, for every port. - ---- - -## Audit Methodology - -**Tool**: Roslyn-based Semantic Analyzer + manual code inspection. -**Scope**: 48 GraphStage implementations across Decoding/, Encoding/, Features/, and Routing/ namespaces. -**Verification**: Checked all `SetHandler` calls for presence of: -- `onUpstreamFinish` (completion) -- `onUpstreamFailure` (exception handling with termination) -- `onDownstreamFinish` (cancellation handling) - -**Date**: 2026-03-27 -**Verified Clean**: All HTTP/1.x stages, Feature BidiStages, core multiplexing stages. - -## See Also - -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Port naming and stage lifecycle conventions -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Where stages fit in the overall design -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]] — Three-layer decoder pattern diff --git a/notes/Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS.md b/notes/Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS.md deleted file mode 100644 index fc56176b0..000000000 --- a/notes/Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS.md +++ /dev/null @@ -1,595 +0,0 @@ ---- -title: Connection Pool Actor Hierarchy Analysis -description: >- - Deep analysis of three actor hierarchy options for TurboHTTP connection - management -tags: - - architecture - - actors - - concurrency - - transport - - connection-pool -aliases: - - ActorHierarchy - - PoolDesign ---- -# Connection Pool Actor Hierarchy Analysis - -**Status**: Complete analysis with recommendation -**Date**: 2026-04-03 -**Context**: TurboHTTP connection pooling actor design decision - ---- - -## Executive Summary - -**Recommendation: OPTION B — Hierarchical (Child per endpoint, not per version)** - -Option B provides the best balance of: -- **Throughput/latency**: Single mailbox per endpoint, no contention on TCP side -- **Fault isolation**: One host failure doesn't affect others -- **Code clarity**: Separate actors make per-host behavior explicit -- **Testing**: Easier to unit test host-specific logic -- **QUIC complexity**: Doesn't escalate child actor complexity (QUIC already manages streams internally) - -The key insight: **QUIC lifecycle complexity lives inside QuicConnectionManager (non-actor), not in the actor hierarchy.** Option B avoids over-engineering by keeping the actor layer minimal. - ---- - -## Current State - -### Existing Architecture -- Single `ConnectionManagerActor` managing all host:port combinations -- Internal `Dictionary` per host -- TCP/QUIC establishment via `DirectConnectionFactory.EstablishAsync()` + `PipeTo` -- GraphStage (`ConnectionStage`) calls actor's `AcquireAsync()` static method -- Inbound pump runs in GraphStage (zero-copy), not actor - -### Message Types Handled -```csharp -AcquireMsg(Options, Endpoint, TaskCompletionSource, CancellationToken) -ReleaseMsg(ConnectionLease, CanReuse) -EstablishedMsg(Lease, Original) -EstablishFailedMsg(Exception, Original) -EvictMsg (periodic idle cleanup) -``` - -### Current Strengths -- Single mailbox = no inter-host message routing -- All state changes in one place (easier to reason about) -- HostState is lightweight (not an actor, just POCO) -- HTTP/1.1 6-conn limit enforced simply via `host.MaxConnections` check - -### Current Weaknesses -- **High contention under load**: Every Acquire/Release from any host queues on one mailbox -- **Fault coupling**: Connection failure in one host could cascade effects (though isolated by HostState) -- **Memory overhead scaling**: Dictionary grows with distinct endpoints, but no per-endpoint resource isolation - ---- - -## Three Options Analysis - -### OPTION A: Flat (Current) - -``` -┌─────────────────────────────────────────┐ -│ ConnectionManagerActor (root) │ -│ ├─ Dictionary │ -│ │ ├─ endpoint.example.com:80 │ -│ │ │ ├─ idle queue │ -│ │ │ ├─ pending queue │ -│ │ │ └─ active leases │ -│ │ ├─ endpoint.example.com:443 │ -│ │ └─ api.other.com:443 │ -│ └─ Periodic eviction timer │ -└─────────────────────────────────────────┘ -``` - -#### Advantages -1. **Zero message hops**: Direct dictionary lookup -2. **Global eviction timer**: Touches all hosts in one pass (O(n) once per timeout) -3. **Atomic cross-host decisions**: Could theoretically prioritize eviction by age -4. **Minimal supervision overhead**: No child actor supervision protocol - -#### Disadvantages -1. **Mailbox contention** - - N hosts × M requests/sec = N×M messages queued on single mailbox - - Under 1000 req/sec across 5 hosts = 200 messages/sec per host on shared queue - - Actor processes them FIFO; if host A stalls (establish delay), hosts B-E wait - -2. **No fault isolation** - - Connection failure in one host can trigger cascading pending queue processing - - Pending queue size grows under slow hosts (memory pressure) - - No way to pause one host without pausing all - -3. **Unclear concurrency model** - - Readers of code see single actor but multiple independent per-host state machines - - Easy to accidentally cross-pollinate host logic - -4. **Testing burden** - - Must mock entire manager to test one host's behavior - - No way to isolate host-specific failure scenarios - -5. **Scale concern** (theoretical) - - If app connects to 100+ hosts, single actor becomes bottleneck - - Real-world apps: 2-5 hosts typical, but possible to exceed - -#### Memory Overhead -- Dictionary: ~40 bytes per entry (reference + hash) -- HostState: ~200 bytes (endpoint, limits, queue refs) -- Per-host total: ~240 bytes (negligible) - -#### Latency Impact (High Concurrency) -``` -AcquireMsg latency under 2000 req/sec across 3 hosts: -- Baseline: ~0.5ms (actor mailbox processing) -- Contention: +2-5ms (queueing delay) -- Total: 2.5-5.5ms per acquire (for simple cases) - -QUIC adds spikes: OpenStreamAsync + channel creation can add 1-2ms, -magnified under contention. -``` - ---- - -### OPTION B: Hierarchical (Child per endpoint) - -``` -┌──────────────────────────────────────────┐ -│ ConnectionManagerActor (root/supervisor) │ -│ ├─ Routes AcquireMsg to child by │ -│ │ endpoint key │ -│ ├─ Routes ReleaseMsg to child │ -│ ├─ Spawn child on first request │ -│ └─ Manages child supervision │ -│ │ -│ Child 1: HostConnectionActor(tcp) │ -│ ├─ Endpoint: example.com:80 │ -│ ├─ Idle queue │ -│ ├─ Pending queue │ -│ ├─ Active leases │ -│ └─ Eviction (local timer) │ -│ │ -│ Child 2: HostConnectionActor(tcp) │ -│ ├─ Endpoint: api.other.com:443 │ -│ └─ [same structure] │ -└──────────────────────────────────────────┘ -``` - -#### Advantages -1. **Per-host mailbox**: No contention between hosts - - Host A Acquire doesn't queue behind Host B Release - - Each host processes Acquire/Release in isolation - -2. **Fault isolation** - - Host A connection failure doesn't stall Host B - - Per-child supervision: restart policy per host - - Pending queue only grows for affected host - -3. **Clear semantics** - - Actor per host makes per-host invariants explicit - - Readers immediately understand: this actor owns all state for one endpoint - - Easy to add per-host policies (rate limits, circuit breakers) - -4. **Testing** - - Mock/fake only the child actor for one host - - Test host A behavior independently of B - - Easier to simulate host-specific failures (timeout, disconnect) - -5. **Monitoring** - - PID per host → can monitor by endpoint - - Metrics per ActorRef naturally - -6. **QUIC readiness** - - If future version uses dedicated QUIC child actors, routing already in place - - TCP and QUIC children can coexist without special casing - -#### Disadvantages -1. **Message routing overhead** - - Root actor receives Acquire, looks up child, forwards → +1 hop - - Under 1000 req/sec = ~1000 extra router messages/sec - - Negligible in practice (~0.1ms per route) - -2. **Child actor lifecycle** - - Create child on first request to endpoint - - Supervision: what if child crashes? - - Options: - - Default restart (OneForOneStrategy): child restarts, pending queue lost - - Stop without restart: pending callers get ObjectDisposedException - - Manual recovery: root reschedules pending to new child - -3. **Global eviction becomes two-level** - - Root timer fires, broadcasts EvictMsg to all children - - Children evict locally, report back - - Still O(n) but with more message passing - -4. **Slightly more code** - - Extract HostConnectionActor logic - - Root actor becomes router + supervisor - - ~50-100 lines of additional boilerplate - -#### Memory Overhead -- Root actor: ~50 bytes -- Child actor per host: ~80 bytes (ActorRef, supervision data) -- Dictionary per child: ~40 bytes -- Per-host total: ~170 bytes for actor + routing vs ~240 for flat -- **Memory savings**: ~20% if many hosts, negligible if few hosts - -#### Latency Impact (High Concurrency) -``` -AcquireMsg latency under 2000 req/sec across 3 hosts: -- Root routing: ~0.1ms -- Child processing: ~0.5ms (no contention) -- Total: 0.6ms per acquire - -No queueing delay because each child has its own mailbox. -QUIC opens + channel creation: same 1-2ms, but not multiplied by other hosts. -``` - ---- - -### OPTION C: Hybrid (Flat for TCP, Children for QUIC) - -``` -┌─────────────────────────────────────────────┐ -│ ConnectionManagerActor (root/supervisor) │ -│ ├─ Dictionary │ -│ │ ├─ endpoint.example.com:80 │ -│ │ └─ endpoint.api.other.com:443 │ -│ └─ For QUIC: spawns child QuicHostActor │ -│ per endpoint │ -│ │ -│ Child 1: QuicHostConnectionActor │ -│ ├─ Endpoint: quic.example.com:443 │ -│ ├─ Shared QuicConnectionManager (non-actor)│ -│ ├─ OpenStreamAsync calls │ -│ └─ Inbound accept loop │ -└─────────────────────────────────────────────┘ -``` - -#### Advantages -1. **Avoids over-engineering TCP**: TCP is simple (6 idle, clear reuse logic) - - Flat model works fine for HTTP/1.0, 1.1, 2.0 - - Contention is low (most apps hit 2-3 TCP hosts) - -2. **Isolates QUIC complexity**: HTTP/3's stream model different - - Multi-stream sharing, control streams, push - - Child actor + QuicConnectionManager separation of concerns - - Can future-proof QUIC without touching TCP code - -3. **Pragmatic**: Matches complexity to protocol - - Simple thing stays simple - - Complex thing gets actor boundaries - -4. **Zero TCP contention**: Dictionary lookup (no child routing) - - QUIC hits actor routing, but QUIC is rare in practice - -#### Disadvantages -1. **Inconsistent design**: Two different models for same problem - - Readers ask: "Why are TCP and QUIC different?" - - Harder to reason about overall architecture - - Harder to maintain: TCP changes don't propagate to QUIC logic - -2. **QUIC code duplication**: If TCP went hierarchical later, QUIC logic duplicates - - Can't easily unify under one supervision strategy - -3. **Testing complexity**: Two different actor models to test - - Unit tests for flat TCP path - - Separate tests for QUIC child path - - Integration tests must cover both - -4. **Marginal performance gain** - - TCP contention only matters at extreme scale (100+ reqs/sec per host) - - Real-world HTTP clients: 10-50 req/sec per host typical - - Hybrid only saves 0.1-0.2ms in corner cases - -5. **Future-proofing fails**: If we later want circuit breakers / per-host rate limits - - Must refactor TCP side too - - Inconsistency bites back - ---- - -## Comparison Matrix - -| Criterion | OPTION A | OPTION B | OPTION C | -|-----------|----------|----------|----------| -| **Throughput (low req/sec)** | 5.0ms | 0.6ms | 5.0ms TCP / 1.0ms QUIC | -| **Throughput (high req/sec, many hosts)** | 10-20ms | 1-2ms | 8-15ms | -| **Fault isolation** | None | Per-host | Per-QUIC, none for TCP | -| **Code clarity** | Medium | High | Low (split model) | -| **Testability** | Hard | Easy | Medium | -| **Monitoring/ops** | Coarse | Fine-grained | Mixed | -| **QUIC readiness** | Inflexible | Ready | Already special | -| **Memory overhead** | ~240/host | ~170/host | ~200/host (hybrid) | -| **Implementation effort** | 0 (done) | ~4-6 hours | ~3-4 hours | -| **Lines of code change** | 0 | +150-200 | +80-120 | -| **Supervision complexity** | None | Low | Medium (inconsistent) | -| **Eviction model** | Simple timer | Per-child timers | Mixed | - ---- - -## Detailed Recommendation: OPTION B - -### Why Option B Wins - -1. **Best throughput under realistic load** - - Real HTTP client workload: 5-20 hosts, 50-500 req/sec total - - Per-host: 10-100 req/sec typical - - At this scale, per-host mailbox (0.6ms) vastly better than shared (5-10ms under load) - -2. **Fault isolation is valuable in practice** - - Slow DNS on one host shouldn't stall others - - One host's connection timeout doesn't block unrelated requests - - Improves user experience: "failing gracefully per destination" - -3. **Clear semantics match RFC model** - - RFC 9112 (HTTP/1.1) §6.3 & RFC 9113 (HTTP/2) §6.2 both describe per-origin connection management - - Actor-per-origin aligns with RFC concepts - - Future readers understand: "one actor owns one origin" - -4. **Testing becomes straightforward** - - Test slow/failed connections without complex mocking - - Simulate circuit breaker per host later - - Integration tests can target specific host failures - -5. **Monitoring/debugging improved** - - `ActorPath` naturally contains endpoint identity - - Prometheus metrics keyed by ActorRef or Path - - Operations teams can "watch one host's actor" via actor tools - -6. **QUIC doesn't escalate complexity** - - QuicConnectionManager is already non-actor, manages streams internally - - If QUIC child actor needed, it wraps QuicConnectionManager - - No new architectural debt - -### Implementation Sketch (Option B) - -#### Root Actor: `ConnectionManagerActor` -```csharp -internal sealed class ConnectionManagerActor : ReceiveActor -{ - private readonly Dictionary _hostActors = new(); - private readonly TimeSpan _idleTimeout; - - // Routes Acquire to child actor - private void OnAcquire(AcquireMsg msg) - { - var child = GetOrCreateHostActor(msg.Endpoint); - child.Tell(msg); - } - - // Routes Release to child actor - private void OnRelease(ReleaseMsg msg) - { - if (_hostActors.TryGetValue(msg.Lease.Key, out var child)) - { - child.Tell(msg); - } - } - - private IActorRef GetOrCreateHostActor(RequestEndpoint endpoint) - { - if (!_hostActors.TryGetValue(endpoint, out var child)) - { - child = Context.ActorOf( - TcpHostConnectionActor.Props(endpoint, _idleTimeout), - name: HostActorName(endpoint)); - _hostActors[endpoint] = child; - } - return child; - } -} -``` - -#### Child Actor: `TcpHostConnectionActor` -```csharp -internal sealed class TcpHostConnectionActor : ReceiveActor, IWithTimers -{ - private readonly RequestEndpoint _endpoint; - private readonly List _leases = new(); - private readonly Queue _idle = new(); - private readonly Queue _pending = new(); - private int _establishing; - - // Same logic as current HostState + message handlers - private void OnAcquire(AcquireMsg msg) - { - // Current OnAcquire logic, but for this endpoint only - } - - private void OnRelease(ReleaseMsg msg) - { - // Current OnRelease logic - } -} -``` - -#### No Changes to GraphStage -- `ConnectionManagerActor.AcquireAsync()` static method unchanged -- GraphStage doesn't know about child actors -- Routes still call root actor; root forwards - -#### Supervision Strategy -```csharp -protected override SupervisorStrategy SupervisorStrategy() => - new OneForOneStrategy( - maxNrOfRetries: 3, - withinTimeRange: TimeSpan.FromSeconds(10), - decider: ex => ex switch - { - ActorInitializationException => Directive.Stop, - _ => Directive.Restart - }); -``` - -**Question**: What if child crashes? -- **Answer**: Child restart policy (default OneForOne) - - On restart, new child spawned, pending queue lost → callers get timeout/cancellation - - This is acceptable: connection failure is transient anyway - - Caller will retry via RetryBidiStage (RFC 9110 §9.2) - - Better than stalling all hosts - ---- - -## QUIC Considerations - -### Current QuicConnectionManager (non-actor) -- Manages shared QUIC connection per endpoint -- Creates streams internally (uses Lock for thread-safety) -- Handles inbound stream acceptance loop -- Lifecycle: `OpenStreamAsync()`, `DisposeAsync()` - -### Does Option B escalate QUIC? -**No.** QUIC complexity stays inside QuicConnectionManager: -- If we later want per-host QUIC actor supervision, it wraps existing manager -- Actor boundary becomes thin: spawn `QuicHostConnectionActor`, inject manager, forward calls -- Current TcpTransportHandler would have QUIC counterpart - -### Future QUIC Child Actor (Optional) -```csharp -internal sealed class QuicHostConnectionActor : ReceiveActor -{ - private readonly QuicConnectionManager _manager; - - private async Task OnOpenStreamMsg(OpenStreamMsg msg) - { - var lease = await _manager.OpenStreamAsync(msg.StreamType, msg.Ct); - Sender.Tell(new StreamOpenedMsg(lease)); - } -} -``` - -This is **optional**: QUIC can stay non-actor if preferable. Option B doesn't force it. - ---- - -## Testing Implications - -### Unit Tests (Option B) -```csharp -[Fact(Timeout = 5000)] -public async Task TcpHostConnectionActor_Acquires_Idle_Lease_After_Release() -{ - var system = ActorSystem.Create("test"); - var hostActor = system.ActorOf( - TcpHostConnectionActor.Props( - new RequestEndpoint("example.com", 443, new Version(1, 1)), - TimeSpan.FromMinutes(5))); - - // Send Acquire, get back TCS - // Mock DirectConnectionFactory.EstablishAsync - // Verify lease returned -} -``` - -### Integration Tests (Option B) -```csharp -[Fact(Timeout = 15000)] -public async Task ConnectionManagerActor_Routes_To_Child_By_Endpoint() -{ - // Send Acquire to root with endpoint A - // Send Acquire to root with endpoint B - // Verify both children spawned (inspect Sender source) - // Verify no crosstalk between hosts -} -``` - ---- - -## Real-World HTTP Client Patterns - -### Typical App Profile -| Metric | Typical | High-Load | -|--------|---------|-----------| -| Unique endpoints | 2-5 | 10-20 | -| Requests/sec | 50-200 | 1000-5000 | -| Per-endpoint req/sec | 10-50 | 50-500 | -| Connection reuse ratio | 90%+ | 95%+ | -| Expected mailbox contention (Option A) | Low | High | -| Expected latency impact (Option A) | Negligible | 2-10ms per op | -| Expected latency impact (Option B) | Negligible | <1ms per op | - -**Conclusion**: Option B is future-proof without complexity today. - ---- - -## Recommendation Summary - -| Factor | Assessment | -|--------|-----------| -| **Current correctness** | Both A and B correct; QUIC already non-actor | -| **Future-proofing** | B is superior (per-host boundaries) | -| **Performance scalability** | B wins at >200 req/sec across 5+ hosts | -| **Code clarity** | B: explicit per-host invariants | -| **Testing** | B: easier isolation | -| **Operations** | B: better monitoring (per-child metrics) | -| **Implementation cost** | B: 4-6 hours, 150-200 lines | -| **Risk** | B: low (refactoring internal component, no API change) | - ---- - -## Implementation Checklist (Option B) - -- [ ] Create `TcpHostConnectionActor` class - - Copy current `HostState` logic into message handlers - - Add `Props(endpoint, idleTimeout)` factory - - Add `PreStart()` timer setup - - Add `PostStop()` cleanup -- [ ] Update `ConnectionManagerActor` - - Add `Dictionary _hostActors` - - Change `OnAcquire` to route to child - - Change `OnRelease` to route to child - - Keep `OnEvict()` (periodic broadcast to children) - - Update supervision strategy -- [ ] Update `TcpTransportHandler` - - No changes (still talks to root actor) -- [ ] Write unit tests - - Test `TcpHostConnectionActor` in isolation - - Test root actor routing - - Test supervision (child restart, pending recovery) -- [ ] Integration tests - - Verify multi-host isolation - - Verify QUIC still works -- [ ] Documentation - - Update `notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md` - - Add transport section explaining hierarchy - ---- - -## Open Questions - -1. **Should eviction be per-child or global?** - - Per-child: each host runs its own timer (more timers, simpler code) - - Global: root broadcasts EvictMsg (fewer timers, same latency) - - Recommendation: **Per-child** (simpler, each host independent) - -2. **How to handle child restart?** - - OneForOne restart: simple, pending queue lost - - Custom strategy: save pending, replay on restart (complex) - - Recommendation: **OneForOne** (transient failures will retry via caller) - -3. **Should GraphStage routing change?** - - No. Keep static `AcquireAsync()` → talks to root only - - Root handles child routing (transparent to caller) - - Recommendation: **No change** (GraphStage remains ignorant) - -4. **Metrics granularity?** - - Current: host.Address + host.Port - - With actors: can also use ActorPath - - Recommendation: **Keep current**, add optional ActorRef tag - ---- - -## See Also - -- [[Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE|Option B Implementation Guide]] — Step-by-step implementation of the recommended hierarchical architecture -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Actor-free connection pool, Channels I/O, TCP/QUIC, backpressure -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — 7-layer design with strict separation of concerns -- [[Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION|ThreadPool Contention Resolution]] — Related dispatcher optimization for high-concurrency scenarios - -## References - -- **Current**: `/d/GIT/Akka.Streams.Http/src/TurboHTTP/Transport/ConnectionManagerActor.cs` (461 lines) -- **QUIC**: `/d/GIT/Akka.Streams.Http/src/TurboHTTP/Transport/QuicConnectionManager.cs` (377 lines, non-actor) -- **Transport Handler**: `/d/GIT/Akka.Streams.Http/src/TurboHTTP/Transport/TcpTransportHandler.cs` -- **Docs**: `notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md` diff --git a/notes/Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE.md b/notes/Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index a7a80190b..000000000 --- a/notes/Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,847 +0,0 @@ ---- -title: Option B Implementation Guide -description: >- - Step-by-step implementation of hierarchical connection pool with child actors - per endpoint -tags: - - implementation - - actors - - transport -aliases: - - ImplementationGuide ---- -# Option B Implementation Guide - -**Status**: Ready for implementation -**Estimated Effort**: 4-6 hours -**Files to Modify**: 2 (ConnectionManagerActor.cs, new TcpHostConnectionActor.cs) -**Files to Create**: 1 (TcpHostConnectionActor.cs) -**Tests to Update**: 2 (ConnectionPoolTests.cs, ConnectionPoolDeadlockTests.cs) - ---- - -## Architecture Before/After - -### BEFORE (Option A — Flat) -``` -ConnectionStage - ↓ GraphStage.Ask() -ConnectionManagerActor (single) - ├─ OnAcquire() → checks Dictionary[endpoint] - ├─ OnRelease() → updates Dictionary[endpoint] - ├─ Mailbox: A.Acquire, B.Release, A.Release, B.Acquire, ... (interleaved) - └─ All messages on single queue -``` - -### AFTER (Option B — Hierarchical) -``` -ConnectionStage - ↓ GraphStage.Tell() -ConnectionManagerActor (router/supervisor) - ├─ OnAcquire(msg) → GetOrCreateChild(endpoint).Tell(msg) - ├─ OnRelease(msg) → _children[endpoint].Tell(msg) - ├─ Mailbox: router messages only - └─ - ├─ TcpHostConnectionActor(example.com:80) - │ ├─ Mailbox: A.Acquire, A.Release (from host A only) - │ ├─ OnAcquire() → checks local _idle, _leases, _pending - │ ├─ OnRelease() → updates local state - │ └─ Periodic EvictMsg (local timer) - │ - └─ TcpHostConnectionActor(api.other.com:443) - ├─ Mailbox: B.Acquire, B.Release (from host B only) - └─ [same structure as above] -``` - ---- - -## Step-by-Step Implementation - -### Phase 1: Create Child Actor Class (1.5 hours) - -#### File: `TcpHostConnectionActor.cs` (new) - -```csharp -using Akka.Actor; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; - -namespace TurboHTTP.Transport; - -/// -/// Single-host connection manager for TCP (HTTP/1.x, 2.0). -/// Spawned by ConnectionManagerActor, one per RequestEndpoint. -/// All state for a single host:port is kept here. -/// -internal sealed class TcpHostConnectionActor : ReceiveActor, IWithTimers -{ - - // Messages routed from root actor: - // - ConnectionManagerActor.AcquireMsg - // - ConnectionManagerActor.ReleaseMsg - // - Internal: EstablishedMsg, EstablishFailedMsg, EvictMsg - - - private sealed class HostState - { - public readonly RequestEndpoint Endpoint; - public readonly int MaxConnections; - public readonly bool IsHttp10; - - /// All established, not-yet-disposed connections. - public readonly List Leases = []; - - /// HTTP/1.1 idle connections available for reuse. - public readonly Queue Idle = new(); - - /// HTTP/1.1 callers waiting for a connection slot. - public readonly Queue Pending = new(); - - /// Number of in-flight EstablishAsync calls. - public int Establishing; - - public HostState(RequestEndpoint endpoint) - { - Endpoint = endpoint; - IsHttp10 = endpoint.Version is { Major: 1, Minor: 0 }; - MaxConnections = IsHttp10 || endpoint.Version.Major >= 2 ? int.MaxValue : 6; - } - } - - - private sealed record EstablishedMsg(ConnectionLease Lease, ConnectionManagerActor.AcquireMsg Original); - private sealed record EstablishFailedMsg(Exception Ex, ConnectionManagerActor.AcquireMsg Original); - private sealed class EvictMsg { public static readonly EvictMsg Instance = new(); } - - - private readonly HostState _host; - private readonly TimeSpan _idleTimeout; - private const string EvictTimerKey = "evict-idle"; - - public ITimerScheduler Timers { get; set; } = null!; - - - public static Props Props(RequestEndpoint endpoint, TimeSpan idleTimeout) - => Akka.Actor.Props.Create(() => new TcpHostConnectionActor(endpoint, idleTimeout)); - - - public TcpHostConnectionActor(RequestEndpoint endpoint, TimeSpan idleTimeout) - { - _host = new HostState(endpoint); - _idleTimeout = idleTimeout; - - Receive(OnAcquire); - Receive(OnRelease); - Receive(OnEstablished); - Receive(OnFailed); - Receive(_ => OnEvict()); - } - - protected override void PreStart() - { - // Each child runs its own eviction timer (could also use parent's global timer) - Timers.StartPeriodicTimer(EvictTimerKey, EvictMsg.Instance, _idleTimeout, _idleTimeout); - TurboTrace.Connection.Debug( - this, - "TcpHostConnectionActor started for {0}:{1}", - _host.Endpoint.Host, - _host.Endpoint.Port); - } - - - private void OnAcquire(ConnectionManagerActor.AcquireMsg msg) - { - if (msg.Tcs.Task.IsCompleted) - { - return; - } - - var version = msg.Endpoint.Version; - - // HTTP/2+: MRU multiplexing - if (version.Major >= 2) - { - var mru = SelectMru(_host); - if (mru is not null) - { - mru.MarkBusy(); - - if (!msg.Tcs.TrySetResult(mru)) - { - mru.MarkIdle(); - } - else - { - TurboHttpMetrics.ConnectionIdle.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - } - - return; - } - - Establish(_host, msg); - return; - } - - // HTTP/1.0: always new, no limit - if (_host.IsHttp10) - { - Establish(_host, msg); - return; - } - - // HTTP/1.1: prefer idle reuse, then establish if slots available, else queue - while (_host.Idle.TryDequeue(out var idle)) - { - if (idle is { IsAlive: true, Reusable: true }) - { - idle.MarkBusy(); - - if (!msg.Tcs.TrySetResult(idle)) - { - idle.MarkIdle(); - _host.Idle.Enqueue(idle); - } - else - { - TurboHttpMetrics.ConnectionIdle.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - } - - return; - } - - // Stale — dispose and free the slot - _host.Leases.Remove(idle); - idle.Dispose(); - TurboHttpMetrics.ConnectionActive.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - } - - // No idle — check slot budget - if (_host.Leases.Count + _host.Establishing < _host.MaxConnections) - { - Establish(_host, msg); - } - else - { - _host.Pending.Enqueue(msg); - } - } - - private void OnRelease(ConnectionManagerActor.ReleaseMsg msg) - { - var version = msg.Lease.Key.Version; - - // HTTP/1.0: always dispose - if (_host.IsHttp10) - { - _host.Leases.Remove(msg.Lease); - msg.Lease.Dispose(); - TurboHttpMetrics.ConnectionActive.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - return; - } - - // HTTP/2+: decrement stream count; dispose only when no active streams and non-reusable - if (version.Major >= 2) - { - msg.Lease.MarkIdle(); - - if (!msg.CanReuse) - { - msg.Lease.MarkNoReuse(); - } - - if (msg.Lease is { ActiveStreams: <= 0, Reusable: false }) - { - _host.Leases.Remove(msg.Lease); - msg.Lease.Dispose(); - TurboHttpMetrics.ConnectionActive.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - } - - return; - } - - // HTTP/1.1 - msg.Lease.MarkIdle(); - - if (msg.CanReuse && msg.Lease is { IsAlive: true, Reusable: true }) - { - // Direct handoff to a pending caller - while (_host.Pending.TryDequeue(out var pending)) - { - if (!pending.Tcs.Task.IsCompleted) - { - msg.Lease.MarkBusy(); - pending.Tcs.TrySetResult(msg.Lease); - return; - } - } - - // No pending callers — park in idle pool - _host.Idle.Enqueue(msg.Lease); - TurboHttpMetrics.ConnectionIdle.Add(1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - } - else - { - // Not reusable — dispose and free the slot - _host.Leases.Remove(msg.Lease); - msg.Lease.Dispose(); - TurboHttpMetrics.ConnectionActive.Add(-1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - - ServeNextPending(_host); - } - } - - private void OnEstablished(EstablishedMsg msg) - { - _host.Establishing--; - _host.Leases.Add(msg.Lease); - msg.Lease.MarkBusy(); - TurboHttpMetrics.ConnectionActive.Add(1, - new("server.address", _host.Endpoint.Host), - new("server.port", _host.Endpoint.Port)); - - if (!msg.Original.Tcs.TrySetResult(msg.Lease)) - { - // Original caller cancelled — treat as immediate release - OnRelease(new ConnectionManagerActor.ReleaseMsg(msg.Lease, CanReuse: true)); - } - } - - private void OnFailed(EstablishFailedMsg msg) - { - _host.Establishing--; - - if (msg.Ex is OperationCanceledException oce) - { - msg.Original.Tcs.TrySetCanceled(oce.CancellationToken); - } - else - { - msg.Original.Tcs.TrySetException(msg.Ex); - } - - ServeNextPending(_host); - } - - - private void OnEvict() - { - EvictHost(_host); - } - - private void EvictHost(HostState host) - { - if (host.Idle.Count == 0) - { - return; - } - - var now = DateTime.UtcNow; - var fresh = new List(); - var expired = new List(); - - while (host.Idle.TryDequeue(out var idle)) - { - if (!idle.IsAlive || now - idle.LastActivity > _idleTimeout) - { - expired.Add(idle); - } - else - { - fresh.Add(idle); - } - } - - // Keep at least one idle connection per host - if (fresh.Count == 0 && expired.Count > 0) - { - var keeper = expired[0]; - for (var i = 1; i < expired.Count; i++) - { - if (expired[i].IsAlive && expired[i].LastActivity > keeper.LastActivity) - { - keeper = expired[i]; - } - } - - if (keeper.IsAlive) - { - expired.Remove(keeper); - fresh.Add(keeper); - } - } - - foreach (var item in fresh) - { - host.Idle.Enqueue(item); - } - - foreach (var lease in expired) - { - host.Leases.Remove(lease); - lease.Dispose(); - TurboHttpMetrics.ConnectionIdle.Add(-1, - new("server.address", host.Endpoint.Host), - new("server.port", host.Endpoint.Port)); - TurboHttpMetrics.ConnectionActive.Add(-1, - new("server.address", host.Endpoint.Host), - new("server.port", host.Endpoint.Port)); - } - } - - - protected override void PostStop() - { - Timers.CancelAll(); - - foreach (var pending in _host.Pending) - { - pending.Tcs.TrySetException(new ObjectDisposedException( - nameof(TcpHostConnectionActor), - $"Connection manager for {_host.Endpoint} was stopped while requests were pending.")); - } - - _host.Pending.Clear(); - _host.Idle.Clear(); - - foreach (var lease in _host.Leases) - { - lease.Dispose(); - } - - _host.Leases.Clear(); - - TurboTrace.Connection.Debug( - this, - "TcpHostConnectionActor stopped for {0}:{1}", - _host.Endpoint.Host, - _host.Endpoint.Port); - } - - - private void Establish(HostState host, ConnectionManagerActor.AcquireMsg msg) - { - host.Establishing++; - DirectConnectionFactory - .EstablishAsync(msg.Options, msg.Endpoint, msg.Ct) - .PipeTo(Self, - success: lease => new EstablishedMsg(lease, msg), - failure: ex => new EstablishFailedMsg(ex, msg)); - } - - private void ServeNextPending(HostState host) - { - while (host.Pending.TryDequeue(out var next)) - { - if (!next.Tcs.Task.IsCompleted) - { - Establish(host, next); - return; - } - } - } - - private static ConnectionLease? SelectMru(HostState host) - { - ConnectionLease? best = null; - foreach (var lease in host.Leases) - { - if (lease.HasAvailableSlot && (best is null || lease.LastActivity > best.LastActivity)) - { - best = lease; - } - } - - return best; - } -} -``` - ---- - -### Phase 2: Update Root Actor (2 hours) - -#### File: `ConnectionManagerActor.cs` (modifications) - -**Replace the entire class with:** - -```csharp -using Akka.Actor; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; - -namespace TurboHTTP.Transport; - -/// -/// Root connection manager actor that routes Acquire/Release messages -/// to per-host child actors. Each gets its own -/// (or QuicHostConnectionActor in future). -/// -/// Advantages: -/// • No mailbox contention between hosts -/// • Fault isolation: one host failure doesn't affect others -/// • Per-host invariants are explicit (actor per host) -/// • Clear RFC alignment: "one actor owns one origin" -/// -/// -internal sealed class ConnectionManagerActor : ReceiveActor -{ - - internal sealed record AcquireMsg(TcpOptions Options, RequestEndpoint Endpoint, TaskCompletionSource Tcs, CancellationToken Ct); - internal sealed record ReleaseMsg(ConnectionLease Lease, bool CanReuse); - - - private readonly Dictionary _hostActors = new(); - private readonly TimeSpan _idleTimeout; - - - public static Props Props(TimeSpan idleTimeout) - => Akka.Actor.Props.Create(() => new ConnectionManagerActor(idleTimeout)); - - /// - /// Sends an to the manager and returns a - /// that completes when the actor resolves the request. - /// Cancellation is wired directly to the ; - /// the child actor skips already-completed TCS instances on dequeue. - /// - public static Task AcquireAsync( - IActorRef actor, TcpOptions options, RequestEndpoint endpoint, CancellationToken ct = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - if (ct.CanBeCanceled) - { - ct.UnsafeRegister( - static (state, token) => ((TaskCompletionSource)state!).TrySetCanceled(token), - tcs); - } - - actor.Tell(new AcquireMsg(options, endpoint, tcs, ct)); - return tcs.Task; - } - - - public ConnectionManagerActor(TimeSpan idleTimeout) - { - _idleTimeout = idleTimeout; - - Receive(OnAcquire); - Receive(OnRelease); - } - - - private void OnAcquire(AcquireMsg msg) - { - var child = GetOrCreateHostActor(msg.Endpoint); - child.Tell(msg); - } - - private void OnRelease(ReleaseMsg msg) - { - if (_hostActors.TryGetValue(msg.Lease.Key, out var child)) - { - child.Tell(msg); - } - } - - - protected override void PostStop() - { - // Child actors will handle their own cleanup via PostStop - _hostActors.Clear(); - } - - - protected override SupervisorStrategy SupervisorStrategy() => - new OneForOneStrategy( - maxNrOfRetries: 3, - withinTimeRange: TimeSpan.FromSeconds(10), - decider: ex => ex switch - { - ActorInitializationException => Directive.Stop, - ObjectDisposedException => Directive.Stop, - _ => Directive.Restart - }); - - - private IActorRef GetOrCreateHostActor(RequestEndpoint endpoint) - { - if (!_hostActors.TryGetValue(endpoint, out var child)) - { - var name = HostActorName(endpoint); - child = Context.ActorOf( - TcpHostConnectionActor.Props(endpoint, _idleTimeout), - name: name); - _hostActors[endpoint] = child; - - TurboTrace.Connection.Debug( - this, - "ConnectionManager spawned child actor for {0}:{1} ({2})", - endpoint.Host, - endpoint.Port, - endpoint.Version); - } - - return child; - } - - /// - /// Generates a safe actor name from endpoint. Actor names must be URL-safe. - /// - private static string HostActorName(RequestEndpoint endpoint) - { - // Replace : with - and . with _ to make valid actor names - var safeName = $"{endpoint.Host}_{endpoint.Port}" - .Replace(":", "-") - .Replace(".", "_"); - return safeName; - } -} -``` - -**Key changes:** -- Removed `HostState` class (moved to child) -- Removed all message handlers except `OnAcquire` and `OnRelease` -- Removed eviction logic (now per-child) -- Added `GetOrCreateHostActor()` with lazy spawning -- Added supervision strategy (OneForOne, restart transient failures) -- Added `HostActorName()` for safe actor naming - ---- - -### Phase 3: Update Tests (1 hour) - -#### File: `TurboHTTP.Tests/Transport/ConnectionPoolTests.cs` - -**Add new test class at end of file:** - -```csharp -[Fact(Timeout = 5000)] -[DisplayName("RFC-9112-conn-isolation-001: Root actor routes to child by endpoint")] -public async Task ConnectionManager_Routes_Different_Endpoints_To_Different_Children() -{ - // Arrange - var system = ActorSystem.Create("test"); - var rootActor = system.ActorOf(ConnectionManagerActor.Props(TimeSpan.FromMinutes(5))); - - var endpoint1 = new RequestEndpoint("example.com", 443, new Version(1, 1)); - var endpoint2 = new RequestEndpoint("api.other.com", 443, new Version(1, 1)); - - var options = new TlsOptions( - new IPEndPoint(IPAddress.Loopback, 443), - remoteAddress: endpoint1.Host, - serverNameIndication: endpoint1.Host, - maxFrameSize: 16384); - - // Act - var ct = System.Threading.CancellationToken.None; - var acquire1 = ConnectionManagerActor.AcquireAsync(rootActor, options, endpoint1, ct); - var acquire2 = ConnectionManagerActor.AcquireAsync(rootActor, options, endpoint2, ct); - - // Wait for children to be created - await Task.Delay(100); - - // Assert: two different child actors should exist - // (Verify via internal state or actor inspection) - Assert.False(acquire1.IsCompleted); // Would complete when connection established - Assert.False(acquire2.IsCompleted); -} - -[Fact(Timeout = 5000)] -[DisplayName("RFC-9112-conn-isolation-002: Child actor failure doesn't affect siblings")] -public async Task ConnectionManager_Child_Failure_Isolates_To_That_Endpoint() -{ - // Arrange: similar setup as above - // Simulate connection failure on endpoint1 - // Verify endpoint2 still accepts new acquires - - // This is harder to test without internal access, but the principle is: - // Child 1 restart shouldn't queue messages on Child 2's mailbox -} -``` - ---- - -### Phase 4: Run Tests (30 minutes) - -```bash -cd /d/GIT/Akka.Streams.Http/src - -# Build -dotnet build --configuration Release - -# Run transport tests -dotnet test --project TurboHTTP.Tests/TurboHTTP.Tests.csproj -- \ - --filter-namespace "TurboHTTP.Tests.Transport" - -# Run integration tests (full system) -dotnet test --project TurboHTTP.IntegrationTests/TurboHTTP.IntegrationTests.csproj -- \ - --filter-namespace "TurboHTTP.IntegrationTests.H11" -``` - -**Expected result**: All tests pass. Existing tests should not need changes (transparent refactoring). - ---- - -### Phase 5: Documentation (30 minutes) - -#### Update: `notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md` - -**Replace Transport Layer section:** - -```markdown -### Transport Layer (`TurboHTTP/Transport/`) - -**Hierarchical connection pool** — per-endpoint actor with fault isolation: -- `ConnectionManagerActor` — root router/supervisor - - Routes `AcquireMsg` / `ReleaseMsg` to child by endpoint - - Spawns `TcpHostConnectionActor` per RequestEndpoint on first use - - Manages child supervision (OneForOne restart strategy) - -- `TcpHostConnectionActor` — per-host manager - - Owns idle queue, pending queue, active leases for one endpoint - - Processes Acquire/Release in isolation (no cross-host contention) - - Runs local eviction timer - - Handles HTTP/1.0, 1.1, 2.0 (TCP) - -- `QuicConnectionManager` — non-actor QUIC multi-stream manager - - Manages shared QUIC connection per endpoint - - Handles stream spawning, inbound acceptance loop - - (Future) Could be wrapped in child actor, but currently standalone - -- `DirectConnectionFactory` — TCP/TLS connection establishment - - Establishes connection, spawns ByteMover tasks, returns ConnectionLease - - No actor involvement (purely async) - -- `ConnectionLease` — wraps ConnectionHandle + lifecycle - -- `ClientByteMover` — async task pump: TCP/QUIC ↔ Channels - -**Design rationale**: -- Per-host actor boundaries eliminate mailbox contention -- Fault isolation: one host timeout doesn't stall others -- Clear semantics: actor-per-origin matches RFC concepts -- Testing: can mock/isolate individual host behavior -``` - ---- - -## Checklist - -- [ ] Create `TcpHostConnectionActor.cs` -- [ ] Verify file compiles (all logic copied from current HostState) -- [ ] Update `ConnectionManagerActor.cs` - - [ ] Remove HostState class - - [ ] Keep AcquireMsg/ReleaseMsg unchanged - - [ ] Add routing to child actors - - [ ] Remove per-host logic (all in child now) - - [ ] Add supervision strategy -- [ ] Run `dotnet build --configuration Release` -- [ ] Run transport tests: `dotnet test --project TurboHTTP.Tests/TurboHTTP.Tests.csproj -- --filter-namespace "TurboHTTP.Tests.Transport"` -- [ ] Run integration tests: `dotnet test --project TurboHTTP.IntegrationTests/...` -- [ ] Verify GraphStage unchanged (no changes needed) -- [ ] Verify TcpTransportHandler unchanged (still calls root actor) -- [ ] Update documentation -- [ ] Commit message: "REFACTOR: Implement hierarchical connection pool with per-endpoint child actors" - ---- - -## Rollback Plan - -If issues arise: - -1. **Test failures**: Likely missing message handler or routing issue - - Check child actor receives message: add logging - - Check TCS completion in child: verify Tcs.TrySetResult called - -2. **Runtime errors**: Usually supervision/restart issues - - Default OneForOne restart should work - - If pending queue lost, caller will timeout and retry (acceptable) - -3. **Performance regression**: Unlikely but monitor - - Each hop adds ~0.1ms, but eliminates contention (net gain) - -4. **Quick rollback**: Revert both files - - Restore original ConnectionManagerActor.cs - - Delete TcpHostConnectionActor.cs - - Git reset to prior commit - ---- - -## Future Extensions (Post-Implementation) - -### QUIC Child Actor (Optional) -```csharp -internal sealed class QuicHostConnectionActor : ReceiveActor -{ - private readonly QuicConnectionManager _manager; - - private async Task OnOpenStreamMsg(OpenStreamMsg msg) - { - var lease = await _manager.OpenStreamAsync(msg.StreamType, msg.Ct); - Sender.Tell(new StreamOpenedMsg(lease)); - } -} -``` - -### Per-Host Circuit Breaker -```csharp -// Inside TcpHostConnectionActor -private int _consecutiveFailures = 0; -private const int FailureThreshold = 5; - -private void CheckCircuit() -{ - if (_consecutiveFailures >= FailureThreshold) - { - // Trip circuit, reject new acquires with specific error - } -} -``` - -### Per-Host Rate Limiting -```csharp -// Inside TcpHostConnectionActor -private readonly RateLimiter _limiter = new(maxRequestsPerSecond: 100); - -private async Task OnAcquire(AcquireMsg msg) -{ - await _limiter.AcquireAsync(); - // ... rest of logic -} -``` - ---- - -## See Also - -- [[Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS|Connection Pool Hierarchy Analysis]] — Full analysis of the three actor hierarchy options -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Transport layer architecture overview -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Overall layered architecture - -## Time Breakdown - -| Phase | Task | Est. Time | -|-------|------|-----------| -| 1 | Create TcpHostConnectionActor | 90 min | -| 2 | Update ConnectionManagerActor | 120 min | -| 3 | Update/add tests | 60 min | -| 4 | Build and run tests | 30 min | -| 5 | Documentation | 30 min | -| **Total** | | **330 min (5.5 hours)** | - -**Actual time may vary by developer experience level.** - diff --git a/notes/Architecture/Analysis/_INDEX.md b/notes/Architecture/Analysis/_INDEX.md deleted file mode 100644 index 200faf98f..000000000 --- a/notes/Architecture/Analysis/_INDEX.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Analysis Index -description: >- - Index of technical analysis notes — investigations, audits, and migration - plans -tags: - - architecture - - analysis - - index ---- -# Analysis - -Technical investigations, audits, and migration plans for TurboHTTP. - -## Notes - -- [[Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION|HTTP/1.0 Pipeline Reconnection Limitation]] — ExtractOptionsStage emits ConnectItem once — HTTP/1.0 redirect/retry cannot reconnect after connection-close -- [[Architecture/Analysis/08-HTTP2_DECODER_MIGRATION|Http2Decoder Migration Plan]] — Migration from monolithic Http2Decoder to stage-based testing via Http2ProtocolSession -- [[Architecture/Analysis/10-DEADLOCK_ANALYSIS|Deadlock Analysis Catalog]] — Catalog of deadlock patterns discovered and resolved in the Akka.Streams pipeline -- [[Architecture/Analysis/11-STAGE_COMPLETION_AUDIT|Stage Completion Propagation Audit]] — Systematic audit of 48 GraphStage implementations finding 20 completion propagation bugs -- [[Architecture/Analysis/13-CONNECTION_POOL_HIERARCHY_ANALYSIS|Connection Pool Hierarchy Analysis]] — Analysis of connection pool design patterns and hierarchy options -- [[Architecture/Analysis/14-OPTION_B_IMPLEMENTATION_GUIDE|Option B Implementation Guide]] — Implementation guide for the selected connection pool architecture diff --git a/notes/Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring.md b/notes/Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring.md deleted file mode 100644 index 0b3d517f8..000000000 --- a/notes/Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring.md +++ /dev/null @@ -1,113 +0,0 @@ -# Benchmark Run: Transport Layer Refactoring (2026-04-03) - -## Summary - -Conducted comprehensive benchmark run following the transport layer refactoring to verify: -1. No hangs occur during benchmark execution -2. Performance impact of the refactoring -3. Memory allocation patterns - -**Result: SUCCESS** - All benchmarks completed without hanging. - -## Benchmark Configuration - -- **Run Type**: ShortRun (3 warmup, 5 iterations, 32 invocations each) -- **Hardware**: AMD Ryzen 5 7600X 4.70GHz (6 physical, 12 logical cores) -- **Runtime**: .NET 10.0.5 (x64, RyuJIT, GC: Concurrent Workstation) -- **Concurrency Levels**: 1, 4, 16, 64, 256 -- **Payloads**: Light (no body, ~20-byte response) and Heavy (10 KB body) -- **HTTP Versions**: 1.1 and 2.0 - -## Key Results - -### Execution Times -- **TurboHTTP Single Request Benchmarks**: 4:02 (242.77 sec) - 40 benchmarks -- **HttpClient Single Request Benchmarks**: 0:19 (19.35 sec) - 40 benchmarks -- **No hangs, timeouts, or deadlocks observed** - -### Performance Comparison (HTTP/1.1, CL=1, Light Payload) - -| Metric | TurboHTTP | HttpClient | Ratio | -|--------|-----------|-----------|-------| -| Mean Latency | 166.2 μs | 96.2 μs | 1.73x slower | -| Req/sec | 6,017 | 10,399 | 0.58x | -| Allocation | 7.14 KB | 2.63 KB | 2.71x more | - -### Performance Comparison (HTTP/2, CL=1, Light Payload) - -| Metric | TurboHTTP | HttpClient | Ratio | -|--------|-----------|-----------|-------| -| Mean Latency | 205.2 μs | 124.8 μs | 1.64x slower | -| Req/sec | 4,873 | 8,010 | 0.61x | -| Allocation | 9.21 KB | 3.3 KB | 2.79x more | - -### Performance Comparison (HTTP/1.1, CL=256, Light Payload) - -At high concurrency, TurboHTTP shows larger relative overhead: - -| Metric | TurboHTTP | HttpClient | Ratio | -|--------|-----------|-----------|-------| -| Mean Latency | 169.1 μs | 88.2 μs | 1.92x slower | -| Req/sec | 5,914 | 11,342 | 0.52x | - -### Memory Allocation Pattern - -- **Light Payloads (no body)**: TurboHTTP allocates 2.7-2.8x more than HttpClient - - This suggests pipeline overhead for minimal request/response -- **Heavy Payloads (10 KB)**: Allocation overhead shrinks to 1.11x (HTTP/1.1) or 0.21x (HTTP/2) - - Indicates the streaming pipeline is more efficient with larger payloads - - HTTP/2 allocation is actually lower than HttpClient for heavy payloads - -### Latency Percentiles (HTTP/1.1, CL=1, Light Payload) - -| Percentile | TurboHTTP | HttpClient | Delta | -|-----------|-----------|-----------|-------| -| P50 | 165.3 μs | 100.0 μs | +65.3% | -| P95 | 188.3 μs | 104.4 μs | +80.3% | -| P100 | 190.4 μs | 104.9 μs | +81.5% | - -All percentiles consistent across concurrency levels - no tail latency explosion. - -## Analysis - -### Transport Layer Refactoring Impact - -**Positive**: -1. ✓ No hangs or deadlocks during any benchmark run -2. ✓ ActorSystem lifecycle properly managed (disposal working) -3. ✓ Thread dispatcher cleanup functioning correctly -4. ✓ Pipeline draining and backpressure working as designed -5. ✓ Scaling behavior is linear (no degradation at CL=256) - -**Performance Characteristics**: -1. TurboHTTP is 1.4-1.9x slower than HttpClient baseline -2. HTTP/1.1 has smaller overhead (1.4-1.6x) than HTTP/2 (1.6-1.9x) -3. Heavy payloads show better TurboHTTP performance (narrower gap) -4. Akka.Streams architecture adds ~2.7x memory per small request - -### Root Causes of Overhead - -Based on benchmark profile: -1. **Pipeline overhead**: Each request flows through multiple GraphStage instances -2. **Allocation pattern**: Small payloads incur fixed overhead per request -3. **HTTP/2 complexity**: Multiplexing and frame encoding adds latency - -The overhead is expected for a stream-based architecture handling RFC compliance. - -## Recommendations - -1. **No immediate action required** - Transport layer refactoring is working correctly -2. **Buffer pooling opportunity** - Could reduce allocations by 30-40% for light payloads -3. **HTTP/2 optimization** - Investigate frame batching to reduce latency -4. **Larger payload benchmarking** - Test with 1MB+ bodies where TurboHTTP may excel -5. **Connection reuse scenario** - Current benchmarks create new clients per iteration; test persistent connections - -## Related Notes - -- [[05-BENCHMARK_PATTERNS]] - Benchmark conventions and port assignments -- [[04-CURRENT_STATE_SUMMARY]] - Project status and performance baselines -- [[08-TRANSPORT_LAYER_ARCHITECTURE]] - Connection pool and dispatcher design - -## Tags - -#benchmark #performance #transport-refactoring #http1 #http2 #akka-streams #2026-04-03 \ No newline at end of file diff --git a/notes/Architecture/Benchmarks/Benchmark_2026-04-04_Perf_Optimizations.md b/notes/Architecture/Benchmarks/Benchmark_2026-04-04_Perf_Optimizations.md deleted file mode 100644 index e8b19a405..000000000 --- a/notes/Architecture/Benchmarks/Benchmark_2026-04-04_Perf_Optimizations.md +++ /dev/null @@ -1,121 +0,0 @@ -# Benchmark Run: Performance Optimizations (2026-04-04) - -## Summary - -Benchmark run following three performance optimizations: -1. **Deleted dead code**: `Http20PrependPrefaceStage`, `Http20StreamIdAllocatorStage`, related test files -2. **Inlined stream ID allocation**: Stream ID generation moved into `Http20ConnectionStage` — eliminates one pipeline stage per request -3. **O(1) slot lookup in `GroupByRequestEndpointStage`**: Replaced `List.Find(s => s.SlotId == id)` with `Dictionary` — eliminates O(n) scan per connection affinity lookup - -**Test status**: 3712 unit tests + 790 stream tests — all passing, 0 failures. - -## Benchmark Configuration - -- **Run Type**: ShortRun (3 warmup, 5 iterations, 32 invocations) -- **Hardware**: AMD Ryzen 5 7600X 4.70GHz (6 physical, 12 logical cores) -- **Runtime**: .NET 10.0.5 (x64, RyuJIT, GC: Concurrent Workstation) -- **Concurrency Levels**: 1, 4, 16, 64, 256 -- **Payloads**: Light (no body) and Heavy (10 KB body) -- **HTTP Versions**: 1.1 and 2.0 -- **Streaming**: 1000, 5000, 10000 requests - -## Key Results - -### TurboHTTP Single Request (selected) - -| Concurrency | Payload | Version | Mean | Req/sec | Allocated | -|-------------|---------|---------|------|---------|-----------| -| 1 | light | 1.1 | 172 μs | 5,811 | 7.78 KB | -| 1 | light | 2.0 | 194 μs | 5,161 | 9.74 KB | -| 1 | heavy | 1.1 | 191 μs | 5,248 | 48.98 KB | -| 1 | heavy | 2.0 | 203 μs | 4,932 | 9.96 KB | -| 256 | light | 1.1 | 174 μs | 5,751 | 6.40 KB | -| 256 | light | 2.0 | 195 μs | 5,127 | 9.74 KB | - -### TurboHTTP vs HttpClient — Concurrent (CL=1, light payload) - -| Metric | TurboHTTP H1.1 | HttpClient H1.1 | TurboHTTP H2 | HttpClient H2 | -|--------|---------------|----------------|--------------|---------------| -| Mean | 171 μs | 102 μs | 201 μs | 119 μs | -| Req/sec | 5,834 | 9,769 | 4,977 | 8,371 | -| Allocated | 6.43 KB | 2.68 KB | 6.69 KB | 8.11 KB | - -TurboHTTP is ~1.7x slower than HttpClient at CL=1 (consistent with previous baseline). - -### TurboHTTP vs HttpClient — Concurrent Throughput (light payload) - -| CL | TurboHTTP H1.1 | HttpClient H1.1 | TurboHTTP H2 | HttpClient H2 | -|----|----------------|----------------|--------------|---------------| -| 4 | 21K req/sec | 22K req/sec | 16K req/sec | 22K req/sec | -| 16 | 40K req/sec | 46K req/sec | 31K req/sec | **84K req/sec** | -| 64 | 34K req/sec | 53K req/sec | 27K req/sec | **46K req/sec** | -| 256 | 28K req/sec | 43K req/sec | 24K req/sec | **134K req/sec** | - -HttpClient H2 at CL=256 achieves 134K req/sec (light) vs TurboHTTP 24K req/sec — because HttpClient multiplexes all 256 requests over a small number of connections, while TurboHTTP creates separate per-endpoint substreams. - -### Streaming Throughput (HTTP/1.1) - -| Requests | TurboHTTP | HttpClient | Ratio | TurboHTTP Alloc | HttpClient Alloc | -|----------|-----------|------------|-------|-----------------|-----------------| -| 1,000 | 22.91 ms | 19.96 ms | 1.15x | 5.23 MB | 2.43 MB | -| 5,000 | 137.32 ms | 97.93 ms | 1.40x | 26.16 MB | 12.42 MB | -| 10,000 | 276.58 ms | 193.02 ms | 1.43x | 51.23 MB | 24.36 MB | - -Streaming overhead grows to ~1.4x at scale. Memory is ~2.1x compared to HttpClient across all counts. - -### Heavy Payload Memory Pattern (CL=1, H2) - -| Library | Mean | Allocated | -|---------|------|-----------| -| TurboHTTP | 188 μs | 7.14 KB | -| HttpClient | 157 μs | 50.45 KB | - -TurboHTTP allocates **7x LESS** than HttpClient for H2 heavy payload at CL=1. The pipeline's pooled buffers avoid materialising the response body on the heap. - -## Comparison vs Previous Baseline (2026-04-03) - -Previous run was taken after the transport layer refactoring, before these optimisations. - -| Scenario | Previous | Current | Delta | -|----------|----------|---------|-------| -| H1.1 CL=1 light mean | 166 μs | 172 μs | +3.6% (noise) | -| H2 CL=1 light mean | 205 μs | 201 μs | -2.0% (noise) | -| H1.1 CL=256 light mean | 169 μs | 174 μs | +3.0% (noise) | -| H1.1 CL=1 light alloc | 7.14 KB | 7.78 KB | +9% (noise) | - -All deltas are within measurement noise (±5-15 μs with ShortRun config). The optimisations do not regress measurable latency — the gains are structural: -- One fewer pipeline stage allocation per request (inlined stream ID) -- O(1) affinity slot lookup (eliminates O(n) scan for connection pools with many slots) -- Smaller codebase: 3 stage files + 2 test files deleted - -## Analysis - -### Why latency numbers are similar despite optimisations - -The bottleneck is **Akka actor message passing** (async scheduler), not the eliminated allocations. Stage removal reduces object count but not the number of scheduler ticks. Measurable gains would require profiling at higher concurrency levels or in sustained-throughput scenarios. - -### HttpClient H2 CL=256 anomaly (134K req/sec) - -HttpClient's HTTP/2 multiplexer sends 256 concurrent requests over ~1–2 connections. TurboHTTP currently opens one substream per endpoint (connection-per-slot model). This is a fundamental architectural difference, not a bug. For the HTTP/2 benchmark to be fair, TurboHTTP would need to multiplex multiple logical requests over a single physical H2 connection at the GroupBy level. - -### Streaming overhead - -The ~1.4x streaming overhead at 10K requests and 2.1x allocation ratio are inherent in the `IOutputItem`/`IInputItem` pipeline design. Every response is wrapped in a `DataItem` with a pooled `IMemoryOwner`. This adds a fixed overhead per item that dominates at small payloads. - -## Recommendations - -1. **No regression from current optimisations** — safe to ship -2. **Streaming memory**: The Gen0/Gen1/Gen2 allocations at 10K requests (`4000/2000/1000`) indicate GC pressure from `DataItem` objects. Consider a slab allocator or object pool for `DataItem`. -3. **HTTP/2 throughput gap**: Investigate multiplexing multiple logical requests per substream at the connection level for scenarios with CL > 16. -4. **Profiling target**: Run dotMemory or BenchmarkDotNet with `NativeMemoryProfiler` at CL=64 H2 to understand the 2,330 μs outlier behaviour. -5. **Baseline cadence**: Re-run these benchmarks after any change to the hot path in `Http20ConnectionStage`, `Http20EncoderStage`, or `GroupByRequestEndpointStage`. - -## Related Notes - -- [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring]] — Previous baseline -- [[05-BENCHMARK_PATTERNS]] — Benchmark conventions and port assignments -- [[04-CURRENT_STATE_SUMMARY]] — Project status - -## Tags - -#benchmark #performance #http1 #http2 #akka-streams #optimization #2026-04-04 diff --git a/notes/Architecture/Benchmarks/_INDEX.md b/notes/Architecture/Benchmarks/_INDEX.md deleted file mode 100644 index ea43f0d8b..000000000 --- a/notes/Architecture/Benchmarks/_INDEX.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Benchmarks Index -description: >- - Index of benchmark result notes — historical performance measurements and - comparisons -tags: - - architecture - - benchmarks - - index ---- -# Benchmarks - -Performance benchmark results and historical comparisons for TurboHTTP. - -## Notes - -- [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring|Benchmark 2026-04-03]] — Transport refactoring baseline measurements -- [[Architecture/Benchmarks/Benchmark_2026-04-04_Perf_Optimizations|Benchmark 2026-04-04]] — Performance optimizations follow-up measurements diff --git a/notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md b/notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md deleted file mode 100644 index dcc5a4278..000000000 --- a/notes/Architecture/Design/01-LAYERED_ARCHITECTURE.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Layered Architecture -description: 7-layer design with strict separation of concerns from client API to TCP/QUIC transport -tags: [architecture, design, layers, akka, streams] -aliases: [ArchitectureOverview, LayerDesign, SystemArchitecture] ---- - -# TurboHTTP Layered Architecture - -## Overview - -TurboHTTP implements a **strict layered architecture** with data flowing from user API down through handlers, streams, encoders/decoders, and finally to the transport layer (TCP/QUIC). - -``` -┌─────────────────────────────────────────────────┐ -│ Client Layer (ITurboHttpClient) │ -│ - DI-friendly factory pattern │ -│ - Channel-based API (ChannelWriter/Reader) │ -├─────────────────────────────────────────────────┤ -│ Handlers Layer (TurboHandler) │ -│ - Delegating handler bridge to Akka pipeline │ -├─────────────────────────────────────────────────┤ -│ Hosting Layer (DI Registration) │ -│ - AddTurboHttpClient() extension │ -├─────────────────────────────────────────────────┤ -│ Streams Layer (Akka.Streams GraphStages) │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Four Protocol Engines (1.0, 1.1, 2.0, 3.0) │ │ -│ │ ┌─────────────────────────────────────────┐ │ │ -│ │ │ Encoding/ - Serialize requests │ │ │ -│ │ │ Decoding/ - Parse wire format │ │ │ -│ │ │ Features/ - Cross-cutting (cache, │ │ │ -│ │ │ redirect, retry, cookies) │ │ │ -│ │ │ Routing/ - Request multiplexing │ │ │ -│ │ └─────────────────────────────────────────┘ │ │ -├─────────────────────────────────────────────────┤ -│ Protocol Layer (Encoders/Decoders) │ -│ - RFC subfolders (RFC9112, RFC9113, RFC9114) │ -│ - HPACK/QPACK compression │ -│ - Business logic: redirects, retries, cookies │ -├─────────────────────────────────────────────────┤ -│ Transport Layer (Actor-free connection pool) │ -│ - ConnectionPool → HostConnections │ -│ - DirectConnectionFactory → ConnectionLease │ -│ - ClientByteMover (async data pump) │ -│ - TCP / QUIC channels │ -└─────────────────────────────────────────────────┘ -``` - -## Layer Responsibilities - -### Client Layer (`TurboHTTP/Client/`) -- **ITurboHttpClient**: Channel-based API - - `ChannelWriter` — requests - - `ChannelReader` — responses - - `SendAsync()` convenience method - - `BaseAddress`, `DefaultRequestVersion`, `DefaultRequestHeaders` -- **ITurboHttpClientFactory**: DI-friendly named/typed client registration -- **TurboHttpClientFactoryExtensions**: Extension methods for factory setup -- **TurboClientOptions**: Per-client config (timeouts, redirects, retries) -- **TurboClientStreamManager**: Akka stream lifecycle management - -### Handlers Layer (`TurboHTTP/Handlers/`) -- **TurboHandler**: Delegating handler that bridges `HttpMessageHandler` → Akka stream pipeline -- **TurboHttpClientBuilder**: Fluent API for composing handler pipeline -- **TurboClientDescriptor**: Configuration snapshot for a client instance - -### Hosting Layer (`TurboHTTP/Hosting/`) -- **TurboClientServiceCollectionExtensions**: DI registration -- Integrates with `IServiceCollection` (Microsoft.Extensions.DependencyInjection) -- Supports named and typed client registration - -### Streams Layer (`TurboHTTP/Streams/`) - -Four separate **protocol engines** route requests by HTTP version: - -#### Encoding/ — Request Serialization -- `Http10EncoderStage`, `Http11EncoderStage`, `Http20EncoderStage`, `Http30EncoderStage` -- `Request2FrameStage` (HTTP/2), `Http30Request2FrameStage` (HTTP/3) -- `PrependPrefaceStage` — HTTP/2 connection preface ("PRI * HTTP/2.0\r\n...") -- `QpackEncoderStreamStage` — QPACK encoder stream (HTTP/3 only) - -#### Decoding/ — Response Parsing -- `Http10DecoderStage`, `Http11DecoderStage`, `Http20DecoderStage`, `Http30DecoderStage` -- `Http20ConnectionStage`, `Http30ConnectionStage` — connection-level frames (SETTINGS, PING, GOAWAY) -- `Http20StreamStage`, `Http30StreamStage` — stream-level assembly into `HttpResponseMessage` -- `QpackDecoderStreamStage` — QPACK decoder stream (HTTP/3 only) - -#### Features/ — Cross-Cutting BidiStages -- **Redirect** (`RedirectBidiStage`) — RFC 9110 §15.4 redirect following -- **Retry** (`RetryBidiStage`) — RFC 9110 §9.2 idempotent retry -- **Cookies** (`CookieBidiStage`) — RFC 6265 cookie injection/storage -- **Cache** (`CacheBidiStage`) — RFC 9111 cache lookup/storage -- **Decompression** (`DecompressionBidiStage`) — gzip/deflate/brotli response body decompression -- **Request Compression** (`RequestCompressionBidiStage`) — request body compression -- **Expect-Continue** (`ExpectContinueBidiStage`) — 100-continue protocol -- **Connection Reuse** (`ConnectionReuseStage`) — keep-alive/close decisions -- **Handler Bridge** (`HandlerBidiStage`) — delegating handler integration - -#### Routing/ — Request Multiplexing & Correlation -- `RequestEnricherStage` — applies BaseAddress, DefaultRequestVersion, DefaultRequestHeaders -- `ExtractOptionsStage` — separates transport options from request -- `Http1XCorrelationStage` — FIFO request-response matching (HTTP/1.x) -- `Http20CorrelationStage` — stream-ID-based matching (HTTP/2) -- `StreamIdAllocatorStage` — allocates client stream IDs (1, 3, 5, …) -- `GroupByHostKeyStage` / `HostKeyMergeBack` — per-host sub-stream routing - -### Protocol Layer (`TurboHTTP/Protocol/`) - -**Encoders** — Serialize `HttpRequestMessage` → bytes: -- Use `ref Span` and `ref Memory` for zero-allocation patterns -- Methods: `Encode()`, `EncodeHeaders()`, etc. - -**Decoders** — Stateful, handle partial frames across TCP boundaries: -- Maintain `_remainder` for incomplete messages -- `TryDecode()` for normal parsing, `TryDecodeEof()` for connection close -- `Reset()` to clear state between connections - -**HPACK (RFC 7541)** — Header compression for HTTP/2: -- `HpackEncoder`/`HpackDecoder` maintain synchronized dynamic tables -- `HpackDynamicTable` — FIFO with 32-byte per-entry overhead -- `HuffmanCodec` — static Huffman encoding/decoding -- Sensitive headers (Authorization, Cookie) use NeverIndex automatically - -**QPACK (RFC 9204)** — Header compression for HTTP/3: -- `QpackDecoder`/`QpackDecoderInstructionWriter` -- Streamed decoder, supports blocking references to encoder updates - -**HTTP/2 Frames** (`Http2Frame.cs`) — 9-byte headers + variable-length payloads: -- `DataFrame`, `HeadersFrame`, `ContinuationFrame`, `RstStreamFrame`, `SettingsFrame`, `PingFrame`, `GoAwayFrame`, `WindowUpdateFrame`, `PushPromiseFrame` -- `SerializedSize` for buffer pre-allocation, `WriteTo(ref Span)` for serialization - -**HTTP/3 Frames** (`RFC9114/Http3Frame.cs`) — Variable-length headers using QUIC integers: -- `Http3FrameEncoder`/`Http3FrameDecoder` -- `Http3RequestEncoder`/`Http3ResponseDecoder` -- Stream types: Control, Request (unidirectional), Bidirectional - -**Business Logic**: -- `RedirectHandler` — RFC 9110 §15.4 redirect following with correct method rewriting -- `RetryEvaluator` — RFC 9110 §9.2 idempotency-based retry -- `ConnectionReuseEvaluator` — RFC 9112 §9 keep-alive/close decision -- `CookieJar` — RFC 6265 domain/path matching, Secure/HttpOnly/SameSite -- `ContentEncodingDecoder` — gzip/deflate/brotli decompression -- `HttpCacheStore` — RFC 9111 thread-safe in-memory LRU cache -- `CacheFreshnessEvaluator` — RFC 9111 freshness lifetime calculation -- `CacheValidationRequestBuilder` — RFC 9111 conditional request building - -### Transport Layer (`TurboHTTP/Transport/`) - -**Actor-free connection pool** — zero mailbox hops: -- `ConnectionPool` — thread-safe async pool; owns nested `HostConnections` per host:port -- `HostConnections` — per-host limits, idle queue, MRU selection -- `DirectConnectionFactory` — establishes TCP/QUIC connections -- `QuicConnectionManager` — QUIC multi-stream management -- `ConnectionLease` — wraps `ConnectionHandle` + lifecycle -- `ClientByteMover` — async task pump: TCP ↔ Channels -- `ClientState` — holds TCP stream, Pipes, channel readers/writers - -**Data Path** — `System.Threading.Channels`: -- `ConnectionStage` acquires `ConnectionLease` from `ConnectionPool` -- `ClientByteMover` spawns as background async tasks per connection -- TCP/QUIC data flows through `System.IO.Pipelines.Pipe` - -## Actor-Based Stream Lifecycle (`TurboHTTP/Client/`) - -The Akka stream pipeline is supervised by a two-actor hierarchy: - -``` -ClientStreamOwnerActor (supervisor) -└── ClientStreamInstanceActor (materializes the Akka.Streams pipeline) -``` - -### ClientStreamOwnerActor -- **Supervises** the stream instance actor -- **Tracks pending work** from feature BidiStages (redirect/retry re-injections) -- **Retries** with exponential backoff: 100ms → 500ms → 2s (max 3 attempts) -- **Graceful shutdown**: 5s timeout, waits for pending work to drain - -### ClientStreamInstanceActor -- **Owns and materializes** the Akka.Streams pipeline (`ChannelSource → Engine → Sink`) -- **Reports** completion/failure to Owner actor -- **Cleans up** resources in `PostStop` - -### Supporting Types -- **IPendingWorkTracker / PendingWorkTracker** — thread-safe lock-free counter; feature BidiStages increment before re-injection, decrement after round-trip; Owner checks before allowing stream completion -- **IClientStreamOwner** — public interface for advanced users; provides `InitializeStreamAsync` and `ActorRef` access -- **StreamInitializationOptions** — record with `TurboClientOptions`, `RequestOptionsFactory`, optional `SupervisorStrategy` -- **StreamInitializationResult** — union type: `Success(IActorRef)` or `Failed(Exception)` - -### Actor Protocol Messages (`ActorProtocol.cs`) -- **ClientStreamOwner.Message**: `Create`, `Created`, `Failed`, `PendingWorkSignal`, `RequestStreamIdle`, `Shutdown` -- **ClientStreamInstance.Message**: `Initialize`, `Initialized`, `Failed`, `PendingWorkChanged`, `RequestShutdown` - -## Key Invariants - -1. **No actor mailbox in data path** — TCP→Channels→Pipe→Channels→TCP with zero actor hops -2. **Layered dependencies** — each layer only depends on layers below it -3. **RFC alignment** — Protocol layer is the RFC authority; Streams/Handlers layer delegates to it -4. **Memory efficiency** — `Span`, `Memory`, `IMemoryOwner` throughout -5. **Cancellation** — `CancellationToken` flows through all async call chains - -## Extension Points - -1. **Custom handlers** — extend `HttpMessageHandler` and add to `TurboHttpClientBuilder` -2. **Custom stages** — extend `GraphStage<>` and wire into `ProtocolCoreGraphBuilder` -3. **Custom encoders/decoders** — replace encoder/decoder implementations (but maintain RFC compliance) -4. **DI configuration** — `AddTurboHttpClient()` extensibility for custom registrations diff --git a/notes/Architecture/Design/02-STAGE_PATTERNS.md b/notes/Architecture/Design/02-STAGE_PATTERNS.md deleted file mode 100644 index 36f07671b..000000000 --- a/notes/Architecture/Design/02-STAGE_PATTERNS.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -title: Stage Patterns -description: GraphStage patterns, port naming conventions, and lifecycle management for Akka.Streams -tags: [patterns, akka, stages, design, conventions] -aliases: [StagePatterns, GraphStagePatterns, PortNaming] ---- - -# TurboHTTP Akka.Streams Stage Patterns - -## Port Naming Convention - -All `GraphStage` inlet/outlet string names follow **PascalCase**: `StageName.Direction` or `StageName.Direction.Role`. - -### String Name Patterns - -| Shape Type | Inlet Pattern | Outlet Pattern | Example | -|-----------|--------------|----------------|---------| -| **FlowShape** (1 in, 1 out) | `StageName.In` | `StageName.Out` | `"Http11Encoder.In"` / `"Http11Encoder.Out"` | -| **FanOutShape** (1 in, 2+ out) | `StageName.In` | `StageName.Out.Role` | `"Redirect.In"` / `"Redirect.Out.Final"` / `"Redirect.Out.Redirect"` | -| **FanInShape** (2+ in, 1 out) | `StageName.In.Role` | `StageName.Out` | `"Http20Correlation.In.Request"` / `"Http20Correlation.In.Response"` | -| **Custom Multi-Port** | `StageName.In.Role` | `StageName.Out.Role` | `"Http20Connection.In.Server"` / `"Http20Connection.Out.Stream"` | - -### C# Field Name Patterns - -| Shape Type | Inlet Fields | Outlet Fields | -|-----------|-------------|--------------| -| **FlowShape** | `_in` | `_out` | -| **FanOutShape** | `_in` | `_outRole` (e.g., `_outFinal`, `_outSignal`) | -| **FanInShape** | `_inRole` (e.g., `_inRequest`) | `_out` | -| **Custom Multi-Port** | `_inRole` | `_outRole` | - -### Naming Rules - -1. **PascalCase throughout** — matches C# idiom -2. **No protocol prefix** — stage class name already contains it (e.g., `Http11Encoder` not `Http.Http11Encoder`) -3. **Drop `Stage` suffix** — string name uses `Http11Encoder`, not `Http11EncoderStage` -4. **Semantic roles** — `Request`, `Response`, `Final`, `Retry`, `Redirect`, `Signal`, `Miss`, `Hit`, `Server`, `Stream`, `App` -5. **Globally unique** — no two stages share the same port string name - -### Examples - -**Http11EncoderStage** (FlowShape): -```csharp -private readonly Inlet _in = new("Http11Encoder.In"); -private readonly Outlet _out = new("Http11Encoder.Out"); -``` - -**RedirectBidiStage** (FanOutShape — redirects are retry-like): -```csharp -private readonly Inlet<(HttpRequestMessage, TransportOptions)> _in = new("Redirect.In"); -private readonly Outlet<(HttpRequestMessage, TransportOptions)> _outFinal = new("Redirect.Out.Final"); -private readonly Outlet<(HttpRequestMessage, TransportOptions)> _outRedirect = new("Redirect.Out.Redirect"); -``` - -**Http20CorrelationStage** (FanInShape): -```csharp -private readonly Inlet _inRequest = new("Http20Correlation.In.Request"); -private readonly Inlet<(int StreamId, HttpResponseMessage)> _inResponse = new("Http20Correlation.In.Response"); -private readonly Outlet<(HttpRequestMessage, HttpResponseMessage)> _out = new("Http20Correlation.Out"); -``` - -## Common Stage Patterns - -### 1. Encoder Stage Pattern (FlowShape) - -**Purpose**: Serialize domain objects → bytes - -```csharp -public sealed class Http11EncoderStage : GraphStage> -{ - private readonly Inlet _in = new("Http11Encoder.In"); - private readonly Outlet _out = new("Http11Encoder.Out"); - - public override FlowShape Shape => - new(_in, _out); - - protected override GraphStageLogic CreateLogic(Attributes attributes) => - new Logic(this, _in, _out); - - private sealed class Logic : InHandler, OutHandler - { - private readonly Http11Encoder _encoder = new(); - - public void OnPush() - { - var request = Grab(_in); - var encoded = _encoder.Encode(request); - Push(_out, ByteString.FromBytes(encoded)); - } - - public void OnPull() => Pull(_in); - - public void OnUpstreamFinish() => CompleteStage(); - public void OnDownstreamFinish() => FailStage(new OperationCanceledException()); - } -} -``` - -**Responsibilities**: -- Maintain stateless or minimal state (`_encoder` is OK, but avoid large buffers) -- Use `Grab()` to consume exactly one element -- Use `Push()` to emit one element per Pull -- Handle upstream finish and downstream finish - -### 2. Decoder Stage Pattern (FlowShape) - -**Purpose**: Parse bytes → domain objects (stateful) - -```csharp -public sealed class Http11DecoderStage : GraphStage> -{ - private readonly Inlet _in = new("Http11Decoder.In"); - private readonly Outlet _out = new("Http11Decoder.Out"); - - public override FlowShape Shape => - new(_in, _out); - - protected override GraphStageLogic CreateLogic(Attributes attributes) => - new Logic(this, _in, _out); - - private sealed class Logic : InHandler, OutHandler - { - private readonly Http11CompletionDecoder _decoder = new(); - - public void OnPush() - { - var chunk = Grab(_in); - if (_decoder.Process(chunk.ToArray()) is {} response) - { - Push(_out, response); - } - else - { - Pull(_in); // Need more data - } - } - - public void OnPull() => Pull(_in); - public void OnUpstreamFinish() => - _decoder.TryDecodeEof() switch - { - { } response => Push(_out, response), - null => CompleteStage() - }; - } -} -``` - -**Responsibilities**: -- Maintain `_remainder` or internal buffer for partial frames -- Call `TryDecode()` when more data arrives -- Pull again if incomplete -- Call `TryDecodeEof()` on upstream finish (connection close) -- Reset state between connections if reusable - -### 3. BidiStage Pattern (BidiShape — Request/Response correlation) - -**Purpose**: Cross-cutting feature that touches both request and response - -Example: **RedirectBidiStage** (actually FanOut for simplicity) - -```csharp -public sealed class RedirectBidiStage : GraphStage> -{ - private readonly Inlet<(HttpRequestMessage, TransportOptions)> _in = new("Redirect.In"); - private readonly Outlet<(HttpRequestMessage, TransportOptions)> _outFinal = new("Redirect.Out.Final"); - private readonly Outlet<(HttpRequestMessage, TransportOptions)> _outRetry = new("Redirect.Out.Retry"); - - public override FanOutShape<...> Shape => new(_in, _outFinal, _outRetry); - - protected override GraphStageLogic CreateLogic(Attributes attributes) => - new Logic(this, _in, _outFinal, _outRetry); - - private sealed class Logic : InHandler, OutHandler - { - private Queue<(HttpRequestMessage, TransportOptions)> _redirectQueue = new(); - private bool _downstreamClosed = false; - - public void OnPush() - { - var (request, opts) = Grab(_in); - if (_redirectHandler.TryGetRedirect(request) is {} redirectUrl) - { - _redirectQueue.Enqueue((newRequest, opts)); - Pull(_in); // Get next request while processing redirect - } - else - { - // No redirect, emit final response - if (!_downstreamClosed) - Push(_outFinal, (request, opts)); - } - } - } -} -``` - -**Key Pattern**: -- Use `Inlet` + `Outlet` for typed channels -- `Grab()` to consume, `Push()` to emit -- `Pull()` to signal ready for more -- Handle backpressure (when downstream can't accept) - -### 4. Connection/Stream Stage Pattern (Multi-Port Custom Shape) - -**Purpose**: Manage connection-level or stream-level protocol state - -Example: **Http20ConnectionStage** (handles SETTINGS, PING, GOAWAY) - -```csharp -public sealed class Http20ConnectionStage : GraphStage> -{ - private readonly Inlet _inServer = new("Http20Connection.In.Server"); - private readonly Outlet _outStream = new("Http20Connection.Out.Stream"); - - protected override GraphStageLogic CreateLogic(Attributes attributes) => - new Logic(this, _inServer, _outStream); - - private sealed class Logic : InHandler, OutHandler - { - private readonly Http2ConnectionState _connState = new(); - - public void OnPush() - { - var frame = Grab(_inServer); - - // Handle connection-level frames - switch (frame) - { - case SettingsFrame sf: - _connState.ApplySettings(sf); - // Emit SETTINGS ACK implicitly - break; - case GoAwayFrame gf: - _connState.MarkGoingAway(gf.LastStreamId); - CompleteStage(); - break; - default: - // Stream-level frames pass through - Push(_outStream, frame); - break; - } - } - - public void OnPull() => Pull(_inServer); - } -} -``` - -## Stage Lifecycle - -1. **OnPush()** — called when upstream has data (after `Pull()`) -2. **OnPull()** — called when downstream is ready (or on first demand) -3. **OnUpstreamFinish()** — called when upstream completes (no more data) -4. **OnDownstreamFinish()** — called when downstream cancels -5. **OnAsyncUpstreamFailure()** — error propagation from upstream - -## Anti-Patterns to Avoid - -1. ❌ **Don't buffer unbounded** — use `async` or external state if buffer > 10KB -2. ❌ **Don't call `Grab()` twice** — one `Grab()` per `OnPush()` -3. ❌ **Don't `Push()` without `Pull()`** — always pair them -4. ❌ **Don't ignore backpressure** — respect downstream readiness -5. ❌ **Don't mix thread contexts** — Akka stages are single-threaded per actor -6. ❌ **Don't put actor names in port strings** — stage class name is enough -7. ❌ **Don't reuse stage instances** — create new stage for each flow - -## Testing Pattern - -Use `StreamTestBase` (extends `TestKit`) for stage unit tests: - -```csharp -public sealed class Http11EncoderStageTests : StreamTestBase -{ - [Fact] - public void EncodeSimpleGet() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); - var source = Source.Single(request); - var sink = Sink.Seq(); - - var result = source - .Via(new Http11EncoderStage()) - .To(sink) - .Run(Materializer); - - result.Should().HaveCount(1); - result[0].Should().StartWith("GET / HTTP/1.1"); - } -} -``` diff --git a/notes/Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE.md b/notes/Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE.md deleted file mode 100644 index 087606456..000000000 --- a/notes/Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Decoder Pipeline Architecture -description: >- - Three-layer decoder architecture for HTTP/1.0, HTTP/1.1, and HTTP/2 — - Pipeline, EventAggregator, CompletionDecoder pattern -tags: - - architecture - - decoder - - protocol - - pipeline -aliases: - - Decoder Pipeline - - Three-Layer Decoder ---- -# Decoder Pipeline Architecture - -**Last Updated**: 2026-03-26 -**Status**: ✅ Complete (HTTP/1.0, HTTP/1.1, HTTP/2 all implemented) - -## Three-Layer Pattern - -Each protocol version follows the same three-layer architecture: - -``` -1. Pipeline — Orchestrates frame/field parsing -2. Event Aggregator — Converts event stream → HttpResponseMessage -3. Completion Decoder — Convenience wrapper (Pipeline + Aggregator) -``` - -### Usage Patterns - -| Pattern | Use When | API | -|---------|----------|-----| -| **Event Streaming** | Real-time body streaming, multiplexing | Use Pipeline directly | -| **Complete Response** | Simple request/response | `CompletionDecoder.Process() → HttpResponseMessage?` | - -### Memory Patterns - -- **Zero-Copy**: Body data is slices of input `ReadOnlyMemory`, not buffered -- **ArrayPool**: Headers buffered during parsing, released after complete response - -## Implementations - -### HTTP/1.1 -- `Http11DecoderPipeline` + `Http11EventAggregator` + `Http11CompletionDecoder` - -### HTTP/1.0 -- `Http10DecoderPipeline` + `Http10EventAggregator` + `Http10CompletionDecoder` -- Extra: `MarkEof()` for EOF-based body boundaries (HTTP/1.0 has no Content-Length guarantee) - -### HTTP/2 -- `Http2DecoderPipeline` + `Http2EventAggregator` + `Http2CompletionDecoder` -- Extra: `Reset()` for connection reuse (HTTP/2 multiplexes on one connection) diff --git a/notes/Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS.md b/notes/Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS.md deleted file mode 100644 index 7b2e5d83d..000000000 --- a/notes/Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS.md +++ /dev/null @@ -1,437 +0,0 @@ ---- -title: Dispatcher Selection for High-Throughput HTTP/2 Pipeline -date: '2026-04-03' -author: Claude Code -status: research -tags: - - akka-streams - - dispatchers - - http2 - - performance - - threading - - threadpool -related: - - Architecture/Status/04-CURRENT_STATE_SUMMARY - - Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring.md ---- -# Dispatcher Selection for High-Throughput HTTP/2 Pipeline - -## Executive Summary - -TurboHTTP processes 64+ concurrent HTTP/2 requests through Akka.Streams GraphStages, causing ThreadPool contention that leads to deadlocks in BenchmarkDotNet processes. This analysis evaluates all six available Akka.NET dispatcher types to identify the optimal choice for high-throughput stream processing without starving the .NET ThreadPool. - -**Recommendation: ChannelExecutor** — Runs on ThreadPool but dynamically scales it, reducing idle threads and contention. Available in Akka.NET 1.5.x (introduced 1.4.19). - ---- - -## Dispatcher Type Comparison - -### 1. ThreadPoolDispatcher (Default) - -**Threading Model:** -- Schedules all actor work on the global .NET ThreadPool -- All instances share the same ThreadPool resource -- No dedicated threads — leverages TPL infrastructure - -**ThreadPool Interaction:** -- COMPETES directly with application async/await continuations -- Uses same queues as all other ThreadPool workloads -- Can cause starvation under high load (HTTP/2 multiplexing scenario) - -**Thread Management:** -- Managed by .NET runtime -- Automatic thread creation/destruction -- No configurable limits per dispatcher instance - -**Suitability for Streaming:** -- ✗ Poor for 64+ concurrent requests -- Adequate only for low-to-moderate throughput -- No resource isolation — all actors compete equally - -**Configuration:** -```hocon -akka.actor.default-dispatcher = { - type = Dispatcher - throughput = 30 # messages per actor before yielding -} -``` - -**Performance Characteristics:** -- Lowest memory overhead -- Maximum latency variance under load -- High context-switch overhead with many actors - -**When to Use:** -- Simple applications with few concurrent actors -- Low-throughput systems -- Development/testing with predictable load - ---- - -### 2. ForkJoinDispatcher - -**Threading Model:** -- Creates a dedicated thread pool for each dispatcher instance -- Threads are owned by Akka, not shared with .NET runtime -- Configurable thread count per dispatcher - -**ThreadPool Interaction:** -- Does NOT use .NET ThreadPool -- Does NOT compete with async/await continuations -- Separate resource pool — eliminates starvation - -**Thread Management:** -- Akka manages all thread lifecycle -- Threads persist for lifetime of ActorSystem -- Deadlock detection: aborts and replaces threads if deadlock-timeout triggers -- Risk: aggressive deadlock-timeout can lose in-flight work - -**Suitability for Streaming:** -- ✓ Good for isolated streaming pipelines -- ✓ Prevents resource contention with application -- Note: Each dispatcher instance has its own thread pool (memory overhead if multiple instances) - -**Configuration:** -```hocon -my-fork-join-dispatcher { - type = ForkJoinDispatcher - throughput = 30 - dedicated-thread-pool { - thread-count = 32 # or use: parallelism-factor × core-count - deadlock-timeout = 3s # abort stuck threads after 3s - threadtype = background - } -} -``` - -**Performance Characteristics:** -- Eliminates context switching with ThreadPool -- Predictable latency (no TPL variance) -- Higher memory usage (dedicated threads always running) -- Scales well to 64+ concurrent requests - -**When to Use:** -- Streaming pipelines requiring isolation -- High-throughput scenarios with many actors -- Applications where ThreadPool must remain available for application code -- Acceptable memory trade-off for latency predictability - ---- - -### 3. ChannelExecutor (v1.4.19+) - -**Threading Model:** -- Hybrid approach: runs on .NET ThreadPool but with dynamic scaling -- Reuses ThreadPool infrastructure but shrinks pool during low activity -- Acts as a middle ground between default and ForkJoinDispatcher - -**ThreadPool Interaction:** -- Uses .NET ThreadPool infrastructure (no dedicated threads) -- Dynamically adjusts ThreadPool size based on demand -- Reduces idle CPU and thread count during variable load -- "Tremendously reduced idle CPU and max busy CPU even during peak message throughput" - -**Thread Management:** -- Leverages ThreadPool's dynamic scaling mechanisms -- No explicit thread lifecycle management required -- Fewer idle threads than dedicated thread pools -- Works well in containerized environments (Docker, Kubernetes) - -**Suitability for Streaming:** -- ✓ Excellent for high-throughput HTTP/2 with variable load -- ✓ Maintains .NET ThreadPool availability -- ✓ Better scaling than dedicated pools in cloud environments -- ✓ Reduces memory footprint compared to ForkJoinDispatcher - -**Configuration:** -```hocon -akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 # minimum ThreadPool threads - parallelism-factor = 1.0 # multiply by core count - parallelism-max = 64 # maximum threads - } -} -``` - -**Performance Characteristics:** -- "Actually beat the ForkJoinDispatcher and others on performance" -- Lower memory overhead than dedicated pools -- Dynamic scaling reduces contention spikes -- Excellent in Docker and bare metal environments - -**When to Use:** -- High-throughput streaming (HTTP/2 multiplexing) ← **Best for TurboHTTP** -- Variable-load scenarios -- Cloud/containerized deployments -- When memory efficiency matters -- When application needs .NET ThreadPool for other work - ---- - -### 4. PinnedDispatcher - -**Threading Model:** -- Single dedicated thread per actor -- Extreme isolation at resource cost - -**ThreadPool Interaction:** -- No ThreadPool usage -- Actor executes serially on its own thread - -**Thread Management:** -- One thread per actor — very expensive -- Should be used sparingly - -**Suitability for Streaming:** -- ✗ Terrible — would need 64+ threads for 64 concurrent requests -- ✗ GraphStages need many actors internally -- ✗ Massive memory and context-switch overhead - -**When to Use:** -- Specific actors requiring strict serialization (rare) -- Never for pipeline stages - ---- - -### 5. SynchronizedDispatcher - -**Threading Model:** -- Uses current SynchronizationContext -- Primarily for UI applications (WinForms, WPF) - -**ThreadPool Interaction:** -- Context-dependent -- Usually marshals to UI thread - -**Suitability for Streaming:** -- ✗ Not suitable -- ✗ Designed for UI thread affinity -- ✗ Would serialize all stream processing through one thread - -**When to Use:** -- Reactive UI applications only -- Never in backend services - ---- - -### 6. TaskDispatcher - -**Threading Model:** -- TPL-based scheduling -- Similar to default ThreadPoolDispatcher but via explicit TPL APIs - -**ThreadPool Interaction:** -- Also uses .NET ThreadPool -- Alternative implementation path - -**Suitability for Streaming:** -- ✗ Same issues as ThreadPoolDispatcher -- ✗ No advantage over default -- Designed for rare scenarios where ThreadPool isn't accessible - -**When to Use:** -- Never in .NET 10.0 environments -- Obsolete for modern .NET - ---- - -## Comparative Analysis Table - -| Attribute | Default | ForkJoin | ChannelExecutor | Pinned | Sync | Task | -|-----------|---------|----------|-----------------|--------|------|------| -| ThreadPool Shared | YES | NO | Hybrid | NO | Context | YES | -| Competes with App | YES | NO | Minimal | NO | Maybe | YES | -| Memory Overhead | Low | High | Low | Extreme | Low | Low | -| Scaling to 64+ req | Poor | Good | Excellent | Terrible | Poor | Poor | -| HTTP/2 Suitable | Poor | Good | **Excellent** | No | No | No | -| Throughput (p/s) | 4,800 | 5,100 | **5,200+** | N/A | N/A | Similar to Default | -| Idle CPU | Baseline | Continuous | **Dynamic** | Continuous | N/A | Baseline | -| Cloud-Friendly | Yes | No | **Yes** | No | No | Yes | -| Config Complexity | Simple | Medium | Medium | Simple | Simple | Simple | - ---- - -## Root Cause Analysis: Why ThreadPool Starvation Occurs - -With current (default) dispatcher setup: - -1. **HTTP/2 Multiplexing**: 64+ concurrent requests = 64+ actors receiving messages -2. **Akka queues messages** on .NET ThreadPool for each actor -3. **GraphStage processing**: Each stage does async I/O (network frame encoding/decoding) -4. **Async continuations**: `await` operations on network calls also queue to ThreadPool -5. **Contention**: Application code (BenchmarkDotNet harness) waits for ThreadPool threads for its own Tasks -6. **Deadlock**: Akka holds ThreadPool threads waiting for I/O; app code also waiting → circular dependency - -The problem: **Akka and application code compete for the same ThreadPool resource queue**. - ---- - -## Recommendations by Scenario - -### Scenario A: Maximum Performance (TurboHTTP Benchmarks) - -**Use ChannelExecutor** - -Reasoning: -- Dynamic scaling eliminates idle thread waste -- Proven faster than ForkJoinDispatcher in benchmarks -- Maintains ThreadPool availability for BenchmarkDotNet harness -- Reduces memory footprint in process - -Configuration: -```hocon -akka { - actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 # 2x core count - parallelism-max = 128 - } - } -} -``` - ---- - -### Scenario B: Production (TurboHTTP in ASP.NET Core) - -**Use ChannelExecutor** (same as above) - -Reasoning: -- ASP.NET Core already uses ThreadPool for request handling -- ChannelExecutor dynamic scaling reduces contention -- Cloud environments benefit most from lower memory footprint -- Scales well from bare metal to containerized deployments - ---- - -### Scenario C: Maximum Latency Predictability - -**Use ForkJoinDispatcher** (if memory is not a constraint) - -Reasoning: -- Eliminates ThreadPool variance entirely -- Dedicated threads provide consistent latency -- Suitable for ultra-low-latency finance/trading apps -- Trade-off: Higher memory, CPU overhead - -Configuration: -```hocon -akka { - actor.default-dispatcher = { - type = ForkJoinDispatcher - throughput = 30 - dedicated-thread-pool { - thread-count = 32 - deadlock-timeout = 10s - threadtype = background - } - } -} -``` - ---- - -## Implementation for TurboHTTP - -### Current State -- Using default ThreadPoolDispatcher (via `ConfigurationFactory.Empty`) -- No explicit dispatcher configuration -- Experiences ThreadPool contention under high concurrency - -### Proposed Change - -**File:** `/src/TurboHTTP/TurboClientServiceCollectionExtensions.cs` - -Modify `LoggingHocon` to include ChannelExecutor configuration: - -```csharp -private static readonly Config LoggingHocon = ConfigurationFactory.ParseString( - """ - akka.loggers = ["Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"] - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - -Alternatively, for benchmarks specifically: - -**File:** `/src/TurboHTTP.Benchmarks/StreamingThroughputBenchmarks.cs` - -```csharp -private static readonly Config BenchHocon = ConfigurationFactory.ParseString( - """ - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - ---- - -## Expected Improvements - -With ChannelExecutor configured: - -1. **Eliminates ThreadPool contention** — Dynamic scaling reduces idle thread count -2. **Maintains App Availability** — ThreadPool remains available for application code -3. **Faster Benchmarks** — Proven performance advantage in testing -4. **Better Scaling** — Linear scaling to 64+ concurrent requests -5. **Lower Memory** — Fewer idle dedicated threads -6. **Cloud Efficiency** — Better container density in Kubernetes - ---- - -## References - -- **Official Akka.NET Docs:** https://getakka.net/articles/actors/dispatchers.html -- **Akka.NET v1.5.64:** Current TurboHTTP version (ChannelExecutor available since 1.4.19) -- **Benchmark Evidence:** [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring|Benchmark 2026-04-03]] - ---- - -## Summary Table: Which Dispatcher When - -| Use Case | Dispatcher | Reason | -|----------|-----------|--------| -| **TurboHTTP (high-throughput HTTP/2)** | **ChannelExecutor** | Dynamic scaling, proven performance, ThreadPool-friendly | -| Low-throughput systems | Default | Simplicity, adequate for light load | -| Extreme latency control | ForkJoinDispatcher | Eliminates TPL variance | -| UI applications | SynchronizedDispatcher | Thread affinity required | -| Individual actor isolation | PinnedDispatcher | Rare, expensive | - ---- - -## See Also - -- [[Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE|Dispatcher Configuration Guide]] — Detailed configuration and tuning guide -- [[Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE|Dispatcher Quick Reference]] — One-page decision tree and config templates -- [[Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION|ThreadPool Contention Resolution]] — ChannelExecutor migration recommendation -- [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring|Benchmark 2026-04-03]] — Transport refactoring baseline measurements - -## Next Steps - -1. Add ChannelExecutor configuration to ActorSystem bootstrap -2. Run benchmarks with new configuration -3. Monitor ThreadPool thread count during benchmark execution -4. Validate no hangs/deadlocks with 64+ concurrent requests -5. Compare memory profiles before/after -6. Document final configuration in CLAUDE.md diff --git a/notes/Architecture/Design/HTTP3_CONSOLIDATION_PLAN.md b/notes/Architecture/Design/HTTP3_CONSOLIDATION_PLAN.md deleted file mode 100644 index cdcc7d5a4..000000000 --- a/notes/Architecture/Design/HTTP3_CONSOLIDATION_PLAN.md +++ /dev/null @@ -1,240 +0,0 @@ ---- -title: HTTP/3 Consolidation Plan -description: >- - Analysis of Http30Engine's 11-stage structure and a proposed consolidation - path to ~5 stages, informed by lessons from HTTP/1.x unification (Feature 001) -tags: - - architecture - - http3 - - stages - - consolidation - - design -status: proposal -created: '2026-04-10' -feature: Feature-001 (design note only) ---- -# HTTP/3 Consolidation Plan - -> **Context:** This note was written as TASK-001-004 after HTTP/1.0 and HTTP/1.1 were each consolidated from 3 stages into a single unified `ConnectionStage` (TASK-001-001 through TASK-001-003). Lessons from that work directly inform the analysis below. -> -> **Scope:** Design note only. No code changes are proposed for the current feature. This informs a future feature. - -## TL;DR - -HTTP/3 currently uses **11 custom `GraphStage` instances** wired into `Http30Engine`. A principled consolidation can reduce this to **5 stages** by merging encoding, connection management, and QPACK feedback paths — while keeping the QUIC-specific unidirectional stream setup stages separate. Estimated effort: ~150k tokens (comparable to TASK-001-001 + TASK-001-002 combined). - ---- - -## 1. Current 11-Stage Structure - -### 1.1 Encoding Stages (request → wire) - -| # | Stage | File | Shape | Purpose | -|---|-------|------|-------|---------| -| 1 | `Http30Request2FrameStage` | `Encoding/` | 1-in, 2-out (custom `Http30Request2FrameShape`) | Converts `HttpRequestMessage` → `Http3Frame` sequence (HEADERS + DATA) via QPACK. Emits QPACK encoder instructions on second outlet (`Out.Encoder`). | -| 2 | `Http30EncoderStage` | `Encoding/` | `FlowShape` | Serializes `Http3Frame` objects to `NetworkBuffer` bytes via `Http3Frame.WriteTo()`. | -| 3 | `Http30ControlStreamPrefaceStage` | `Encoding/` | `FlowShape` | Emits HTTP/3 control stream preface (stream type VarInt `0x00` + SETTINGS frame) on `PreStart`, then passes items through. Tags output with `OutputStreamType.Control`. | -| 4 | `Http30QpackEncoderPrefaceStage` | `Encoding/` | `FlowShape, IOutputItem>` | Prepends QPACK encoder stream type (VarInt `0x02`) once on first emission, then passes QPACK instructions through. Tags output with `OutputStreamType.QpackEncoder`. | -| 5 | `QpackEncoderStreamStage` | `Encoding/` | `FlowShape>` | Serializes `EncoderInstruction` objects to bytes for the QPACK encoder unidirectional stream (RFC 9204 §4.3). | - -### 1.2 Decoding Stages (wire → response) - -| # | Stage | File | Shape | Purpose | -|---|-------|------|-------|---------| -| 6 | `Http30DecoderStage` | `Decoding/` | `FlowShape` | Deserializes raw bytes to `Http3Frame` objects. Filters unknown frame types (RFC 9114 §7.2.8). | -| 7 | `Http30ConnectionStage` | `Decoding/` | 2-in, 2-out (custom `Http30ConnectionShape`) | HTTP/3 connection-level state machine: SETTINGS/GOAWAY handling, idle timeout (30s default), push promise limits. Consolidated from 7 prior handlers. Routes frames between app and server paths. | -| 8 | `Http30StreamStage` | `Decoding/` | `FlowShape` | Assembles HEADERS + DATA frames → `HttpResponseMessage` using QPACK decoder. Unlike HTTP/2: no stream IDs in frames, no CONTINUATION frames. | -| 9 | `QpackDecoderStreamStage` | `Decoding/` | `FlowShape, DecoderInstruction>` | Deserializes bytes from QPACK decoder unidirectional stream (RFC 9204 §4.4) to `DecoderInstruction` objects. | -| 10 | `QpackDecoderFeedbackStage` | `Decoding/` | `SinkShape` | Applies decoder instructions back to `QpackEncoder` state (Section Acknowledgment, Stream Cancellation, Insert Count Increment). Terminal sink — no output. | - -### 1.3 Routing Stage - -| # | Stage | File | Shape | Purpose | -|---|-------|------|-------|---------| -| 11 | `Http30CorrelationStage` | `Routing/` | `FanInShape` | FIFO correlation of requests and responses. Sets `response.RequestMessage = request`. HTTP/3 preserves per-connection request order. | - -### 1.4 Built-in Operators in Http30Engine (not custom stages, listed for completeness) - -- `Broadcast(2)` — splits requests for frame encoding and correlation -- `Partition(2, ClassifyInputItem)` — separates HTTP/3 frames from QPACK decoder feedback bytes -- `BatchWeighted` — coalesces output buffers up to 65 KB before write -- `Merge(2)` — combines frame bytes and QPACK encoder instruction bytes - ---- - -## 2. Consolidation Targets - -### 2.1 Group A: QPACK Encoder Stream (2 stages → 1) - -**Merge:** `QpackEncoderStreamStage` + `Http30QpackEncoderPrefaceStage` → **`QpackEncoderStreamStage`** - -**Rationale:** -- `Http30QpackEncoderPrefaceStage` has a single responsibility: prepend VarInt `0x02` (QPACK encoder stream type) on the first item, then pass through. This is identical in structure to `Http20PrependPrefaceStage` which was already absorbed directly into the HTTP/2 encoder stage. -- Emitting the stream type byte belongs naturally inside the encoder stream stage as a `_prefaceSent` flag in `PreStart` / on first push — a 5-line change. -- Eliminates one `FlowShape` in the encoding fan-out. - -**Resulting shape:** `FlowShape` (absorbs both serialization and stream-type tagging). - -**Risk:** Low. Pure inline logic with no state shared across stages. - ---- - -### 2.2 Group B: QPACK Decoder Stream + Feedback (2 stages + Partition → 1) - -**Merge:** `QpackDecoderStreamStage` + `QpackDecoderFeedbackStage` + `Partition` routing → **`QpackDecoderStage`** - -**Rationale:** -- The two stages are always wired sequentially with no branching: `Partition.Out1 → QpackDecoderStreamStage → QpackDecoderFeedbackStage`. -- `QpackDecoderFeedbackStage` is a `SinkShape` with zero outputs. The combined stage becomes a `SinkShape>` that parses instructions and applies them inline — eliminating the intermediate `DecoderInstruction` materialization. -- Removes the `Partition(2)` operator from the engine (its QPACK branch disappears; the remaining non-QPACK branch becomes the single input to the decoder). -- Simplifies engine wiring significantly. - -**Resulting shape:** `SinkShape>` (consumes QPACK feedback bytes, applies to encoder, produces nothing). - -**Risk:** Low-medium. The combined stage accesses `QpackEncoder` directly. Must ensure thread safety if the encoder is accessed from both the encoding path and the feedback sink. Akka.Streams fused graphs guarantee single-thread execution within a fused island — verify the QPACK encoder lives in the same island. - ---- - -### 2.3 Group C: Frame Encoding (2 stages → 1) - -**Merge:** `Http30Request2FrameStage` + `Http30EncoderStage` → **`Http30EncoderStage`** - -**Rationale:** -- `Http30Request2FrameStage` outputs `Http3Frame` objects; `Http30EncoderStage` immediately consumes them and outputs `IOutputItem` bytes. There is no other consumer of the intermediate `Http3Frame`. -- Merging eliminates the intermediate materialization of `Http3Frame` structs and the edge between stages. -- The resulting stage takes `HttpRequestMessage` directly and emits `IOutputItem` bytes + QPACK encoder instructions on a second outlet. -- Retains the 2-outlet `Http30Request2FrameShape` but makes the outer type simpler: `In` is `HttpRequestMessage`, `Out.Frame` becomes `Out.Network` (encoded bytes), `Out.Encoder` unchanged. - -**Resulting shape:** Custom 1-in, 2-out: `In` (`HttpRequestMessage`), `Out.Network` (`IOutputItem`), `Out.Encoder` (`EncoderInstruction`). - -**Risk:** Low. Both stages are pure data transformations with no side effects. The merge is additive. - ---- - -### 2.4 Group D: Stream Assembly + Connection + Correlation (3 stages → 1) - -**Merge:** `Http30StreamStage` + `Http30ConnectionStage` + `Http30CorrelationStage` → **unified `Http30ConnectionStage`** - -**Rationale:** -- This is the exact same consolidation performed for HTTP/1.0 (TASK-001-001) and HTTP/1.1 (TASK-001-002), following the established `Http20ConnectionStage` pattern. -- `Http30StreamStage` performs per-stream HEADERS+DATA assembly — analogous to the `Http11StateMachine.DecodeServerData()` function. -- `Http30CorrelationStage` performs FIFO request/response correlation — analogous to `_inFlightQueue` management. -- `Http30ConnectionStage` already houses the `ConnectionState` nested class; stream assembly and correlation become `StreamState` and `_pendingRequests` respectively, following `Http20ConnectionStage` verbatim. -- Removes one `FanInShape` routing stage and simplifies the encoding/decoding split in the engine. - -**Resulting shape:** The existing 4-port `Http30ConnectionShape` is preserved: `In.Server`, `In.App`, `Out.App`, `Out.Server`. The `Out.App` outlet now emits fully-assembled, correlated `HttpResponseMessage` directly. - -**Risk:** Medium. This is the most complex consolidation. `Http30ConnectionStage.ConnectionState` currently handles connection-level signals only; adding stream assembly brings QPACK decoding and response body buffering inside. Care required for: -- QPACK decoder state shared with `QpackDecoderFeedbackStage` (see Group B — resolve Group B first) -- Push promise handling (currently validated at connection level, may interact with stream assembly) -- Memory pool lifetime for response body buffers (must call `Dispose` in `PostStop`) - ---- - -## 3. Stages That Must Remain Separate - -### 3.1 `Http30ControlStreamPrefaceStage` — Keep as-is - -**Reason:** HTTP/3 requires the control stream preface (stream type `0x00` + SETTINGS frame) to be sent on a **dedicated unidirectional stream**, distinct from request streams. The stage tags its output with `OutputStreamType.Control` for demux routing by the transport layer. This tagging logic is control-stream-specific and does not compose naturally with request encoding. Inlining it would introduce transport-layer concerns (stream type tagging) into the encoder. - -**RFC reference:** RFC 9114 §6.2.1 — control stream is a separate QUIC unidirectional stream. - -### 3.2 QPACK as Separate Sub-pipeline (encoder + decoder) - -**Reason:** QPACK encoder instructions and decoder feedback travel on **separate QUIC unidirectional streams** (stream types `0x02` and `0x03`). The encoding and decoding paths are separate from request/response data streams by design. While the stages can be simplified (Groups A and B above), the QPACK sub-pipeline must remain architecturally separate from the request/response sub-pipeline — it cannot be folded into `Http30ConnectionStage` without conflating two independent QUIC stream types. - -**RFC reference:** RFC 9204 §4.2–§4.4. - ---- - -## 4. Proposed Target Architecture - -### 4.1 Stage Count - -| Before | After | -|--------|-------| -| 11 custom stages | 5 custom stages | -| 4 built-in operators | 2 built-in operators (`BatchWeighted`, `Broadcast`) | - -### 4.2 Target Stage List - -| Stage | Consolidated From | Notes | -|-------|-------------------|-------| -| `Http30EncoderStage` | `Http30Request2FrameStage` + `Http30EncoderStage` | Custom 1-in, 2-out shape: `In`, `Out.Network`, `Out.Encoder` | -| `Http30ControlStreamPrefaceStage` | (unchanged) | Must remain separate — see §3.1 | -| `Http30ConnectionStage` | `Http30DecoderStage` + `Http30ConnectionStage` + `Http30StreamStage` + `Http30CorrelationStage` | 4-port shape preserved; absorbs stream assembly and FIFO correlation | -| `QpackEncoderStreamStage` | `QpackEncoderStreamStage` + `Http30QpackEncoderPrefaceStage` | Absorbs preface emission in `PreStart`; emits `IOutputItem` directly | -| `QpackDecoderStage` | `QpackDecoderStreamStage` + `QpackDecoderFeedbackStage` | New `SinkShape>`; removes `Partition` from engine | - -### 4.3 Simplified Engine Wiring - -```text -Encoding Path: - Broadcast(2) - ├── Http30EncoderStage (Out.Network) → BatchWeighted → Http30ControlStreamPrefaceStage - └── Http30EncoderStage (Out.Encoder) → QpackEncoderStreamStage - -Decoding Path: - Http30ConnectionStage (Out.App) → correlated HttpResponseMessage - Http30ConnectionStage (In.Server) ← raw IInputItem bytes - -QPACK Feedback: - inbound QPACK bytes → QpackDecoderStage (sink) -``` - -Compared to current engine: removes `Merge(2)`, `Partition(2)`, `Http30DecoderStage`, `Http30StreamStage`, `Http30CorrelationStage`, `Http30QpackEncoderPrefaceStage`, `QpackDecoderFeedbackStage`. - ---- - -## 5. Recommended Implementation Order - -Tackle Group B first (lowest risk, removes complexity from engine routing), then Group A, then Group C, then Group D last (highest risk, most reward). - -1. **Group B** — `QpackDecoderStage` consolidation: eliminates `Partition`, simplifies engine -2. **Group A** — `QpackEncoderStreamStage` absorbs preface: pure additive change -3. **Group C** — `Http30EncoderStage` absorbs request2frame: eliminates intermediate `Http3Frame` edge -4. **Group D** — Unified `Http30ConnectionStage`: largest change, implement after QPACK is clean - ---- - -## 6. Blockers and Risks - -| Blocker / Risk | Severity | Mitigation | -|----------------|----------|------------| -| QPACK encoder thread-safety between Group C (encoder instruction emission) and Group B (feedback sink) | Medium | Confirm both stages fuse into the same Akka.Streams island. If so, single-threaded execution guarantees eliminate the concern. | -| Push promise handling in `Http30ConnectionStage.ConnectionState` interacts with stream assembly (Group D) | Medium | Push promises are currently validated and rejected at connection level (limit = 0). Stream-level assembly will not see push promise frames in current configuration. Future push support would require revisiting. | -| `Http30Request2FrameShape` is a custom type — callers outside the engine may reference it | Low | Grep confirms it is only referenced inside `Http30Engine.cs`. Safe to replace. | -| QPACK dynamic table is shared state between encoder path and decoder feedback path | Low-Medium | Encapsulate `IQpackEncoder` / `IQpackDecoder` lifecycle within the new `QpackDecoderStage` constructor injection, matching how HTTP/2's `IHpackEncoder` is injected into `Http20ConnectionStage`. | -| HTTP/3 integration tests are slow — full suite regressions may not surface until CI | Low | Run per-class: `dotnet run --project TurboHTTP.IntegrationTests -- -namespace "TurboHTTP.IntegrationTests.H3"` after each group. | -| `MemoryPool` response body buffers in `Http30StreamStage` must be properly disposed when consolidated | Medium | Follow `Http11StateMachine` pattern: call `Dispose` in `PostStop` of the unified `Http30ConnectionStage`. Add dedicated test for teardown during mid-response connection close. | - ---- - -## 7. Effort Estimate - -| Group | Stages Removed | Complexity | Estimated Tokens | -|-------|---------------|------------|------------------| -| A — QpackEncoderStream | 1 | Low | ~15k | -| B — QpackDecoderStage | 2 + Partition | Low-Medium | ~25k | -| C — Http30EncoderStage | 1 + intermediate edge | Low | ~20k | -| D — Http30ConnectionStage | 3 + FanIn | Medium-High | ~90k | -| Tests + cleanup | — | Medium | ~40k | -| **Total** | **7 stages + 2 operators** | — | **~190k** | - ---- - -## 8. Success Metrics - -- Net removal of 7 custom stage files + 1 custom shape class (`Http30Request2FrameShape`) -- `Http30Engine.cs` wiring: from ~80 lines of `GraphDsl` to ~30 lines -- Architectural consistency: `Http30ConnectionStage` follows the same pattern as `Http10ConnectionStage`, `Http11ConnectionStage`, and `Http20ConnectionStage` -- Zero regressions across all test projects - ---- - -## See Also - -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Port naming and stage lifecycle conventions -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — Full pipeline data flow and per-version engine assembly -- `src/TurboHTTP/Streams/Http30Engine.cs` — Current engine wiring -- `src/TurboHTTP/Streams/Stages/Decoding/Http20ConnectionStage.cs` — Pattern to follow for Group D diff --git a/notes/Architecture/Design/_INDEX.md b/notes/Architecture/Design/_INDEX.md deleted file mode 100644 index 8a36ac173..000000000 --- a/notes/Architecture/Design/_INDEX.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Design Index -description: >- - Index of core architectural design notes — layered architecture, stage - patterns, decoder pipeline -tags: - - architecture - - design - - index ---- -# Design - -Core architectural patterns and design decisions for TurboHTTP. - -## Notes - -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — 7-layer design with strict separation of concerns from client API to TCP/QUIC transport -- [[Architecture/Design/02-STAGE_PATTERNS|Stage Patterns]] — GraphStage patterns, port naming conventions, and lifecycle management for Akka.Streams -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]] — Three-layer decoder architecture for HTTP/1.0, HTTP/1.1, and HTTP/2 -- [[Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS|Dispatcher Selection Analysis]] — Evaluation of all six Akka.NET dispatcher types for high-throughput HTTP/2 streaming -- [[Architecture/Design/HTTP3_CONSOLIDATION_PLAN|HTTP/3 Consolidation Plan]] — Plan for consolidating HTTP/3 (QUIC) support into the stage-based architecture diff --git a/notes/Architecture/Guides/05-BENCHMARK_PATTERNS.md b/notes/Architecture/Guides/05-BENCHMARK_PATTERNS.md deleted file mode 100644 index 9af3508c0..000000000 --- a/notes/Architecture/Guides/05-BENCHMARK_PATTERNS.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Benchmark Patterns & Infrastructure -description: >- - BenchmarkDotNet conventions, port assignments, Windows TCP TIME_WAIT - workarounds, thread safety rules for concurrent benchmarks -tags: - - benchmarks - - performance - - infrastructure - - tcp -aliases: - - Benchmark Patterns - - BDN Patterns ---- -# Benchmark Patterns & Infrastructure - -**Last Updated**: 2026-03-26 - -## BenchmarkDotNet Conventions - -Standard attributes for TurboHTTP benchmarks: -```csharp -[MemoryDiagnoser] -[Config(typeof(MicroBenchmarkConfig))] -[SimpleJob(warmupCount: 3, targetCount: 5)] -``` - -**Dry-run command:** -```bash -dotnet run --configuration Release --project src/TurboHTTP.Benchmarks/... -- --filter "*ClassName*" --job dry -``` - -**Key**: BDN runs each benchmark×job in a separate child process; each child calls `GlobalSetup → benchmark → GlobalCleanup`. - ---- - -## Port Assignments - -| Benchmark File | Port | -|----------------|------| -| CoreRequestBenchmarks | 5006 | -| CoreMemoryBenchmarks | 5007 | -| CoreConnectionBenchmarks | 5008 | -| Http11EfficiencyBenchmarks | 5009 | -| ConcurrencyScalingBenchmarks | dynamic (port 0) | -| BurstTrafficBenchmarks | dynamic (port 0) | -| FailureRecoveryBenchmarks | dynamic (port 0) | - ---- - -## Windows TCP TIME_WAIT & Ephemeral Port Exhaustion - -- Windows has ~16,384 ephemeral ports (49152–65535) -- TIME_WAIT lasts 120s by default; each closed connection blocks `(src_ip:src_port, dst_ip:dst_port)` -- BDN pilot phase doubles `invocationCount` until iteration ≥ 500ms → can generate thousands of connections -- **Formula**: `total_connections = (pilot_invocations + warmupCount × invocationCount + targetCount × invocationCount) × conns_per_invocation` -- For a 300µs operation: `invocationCount ≈ 2048`, giving ~20,000 total connections → **exhausts 16,384 limit** - -### Solutions - -1. **Pre-established connection pool**: `GlobalSetup` creates N keep-alive connections; benchmarks reuse them — zero new connections per pilot invocation -2. **Dynamic port**: `web.UseUrls("http://127.0.0.1:0")` then discover via: - ```csharp - _server.Services.GetRequiredService() - .Features.Get()! - ``` - Requires: `Microsoft.AspNetCore.Hosting.Server`, `Microsoft.AspNetCore.Hosting.Server.Features`, `Microsoft.Extensions.DependencyInjection`, `System.Linq` -3. **invocationCount cap**: `[SimpleJob(warmupCount:3, targetCount:5, invocationCount:16)]` — bypasses pilot, caps total connections - ---- - -## Thread Safety in Concurrent Benchmarks - -**Rule**: Never use class-level `_encBuf`/`_readBuf` fields in methods called concurrently. - -**Why**: BDN may run benchmark methods in parallel across threads. - -**Fix**: Use local buffers per call: -```csharp -var encBuf = new byte[512]; -var readBuf = new byte[2048]; -``` diff --git a/notes/Architecture/Guides/09-CLAUDE_PREFERENCES.md b/notes/Architecture/Guides/09-CLAUDE_PREFERENCES.md deleted file mode 100644 index 215e32fba..000000000 --- a/notes/Architecture/Guides/09-CLAUDE_PREFERENCES.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Claude Code Preferences & Workflow Guidelines -description: >- - User preferences for Claude Code interactions — language, documentation style, - knowledge capture workflow, and response format -tags: - - preferences - - workflow - - claude -aliases: - - Preferences - - Claude Guidelines ---- -# Claude Code Preferences & Workflow Guidelines - -**Last Updated**: 2026-03-26 - -## Language - -- **Always respond in English** — regardless of input language -- Feature plans, documentation, code comments, and all outputs: English -- Obsidian notes: English - -## Knowledge Capture - -Every session must document important findings in the Obsidian vault (`notes/`): - -| Discovery Type | Destination | Template | -|----------------|-------------|----------| -| RFC compliance gaps | `notes/RFC/` or `notes/rfc/` | RFC-Note | -| Architecture decisions | `notes/Architecture/` | ADR | -| Protocol limitations or workarounds | `notes/Architecture/` | ADR | -| Bug investigations & root causes | `notes/Debugging/` (git-ignored) | Bug-Investigation | -| Feature learnings | `notes/Features/` | — | -| Session work logs | `notes/Sessions/` (git-ignored) | Session-Log | - -**Before ending session**: Check — did I discover something important that future sessions should know? If yes, create/update an Obsidian note. - -## Response Style - -- Terse responses, no trailing summaries (user reads the diff) -- Go straight to the point -- No emojis unless requested diff --git a/notes/Architecture/Guides/10-TEST_CONVENTIONS.md b/notes/Architecture/Guides/10-TEST_CONVENTIONS.md deleted file mode 100644 index 24278e1f8..000000000 --- a/notes/Architecture/Guides/10-TEST_CONVENTIONS.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Test Conventions -tags: [architecture, testing, conventions] -created: 2026-04-13 -updated: 2026-04-13 ---- - -# Test Conventions - -## Structure (Component-Based, Post-Feature-040) - -Starting with Feature 040, test files are organized by **component/protocol version**, not RFC number: - -| Project | Structure | -|---------|-----------| -| `TurboHTTP.Tests/` | `Http10/`, `Http11/`, `Http2/`, `Http3/`, `Semantics/`, `Caching/`, `Cookies/`, `Transport/`, `Security/`, `Diagnostics/`, `Hosting/` | -| `TurboHTTP.StreamTests/` | `Http10/`, `Http11/`, `Http2/`, `Http3/`, `Semantics/`, `Caching/`, `Cookies/`, `Transport/`, `Dispatchers/`, `Streams/` | -| `TurboHTTP.IntegrationTests/` | Unchanged: `H10/`, `H11/`, `H2/`, `H3/`, `TLS/` | - -## RFC → Component Mapping - -| RFC | Component | Folder | Example | -|-----|-----------|--------|---------| -| RFC 1945 | HTTP/1.0 | `Http10/` | `Http10EncoderSpec.cs` | -| RFC 9112 | HTTP/1.1 | `Http11/` (with `Encoding/`, `Decoding/`, `Chunking/` subfolders) | `Http11ChunkedDecoderSpec.cs` | -| RFC 9113 | HTTP/2 Frames & Streams | `Http2/Frames/`, `Http2/Connection/`, `Http2/Stream/` | `Http2FrameDecoderSpec.cs` | -| RFC 7541 | HPACK | `Http2/Hpack/` | `HpackEncodingSpec.cs` | -| RFC 9114 | HTTP/3 (QUIC) | `Http3/` (with `Frames/`, `Connection/`, `Qpack/` subfolders) | `Http3ConnectionSpec.cs` | -| RFC 9204 | QPACK | `Http3/Qpack/` | `QpackEncodingSpec.cs` | -| RFC 9110 | HTTP Semantics | `Semantics/` | `RedirectHandlingSpec.cs`, `RetryPolicySpec.cs` | -| RFC 9111 | HTTP Caching | `Caching/` | `CacheValidationSpec.cs` | -| RFC 6265 | HTTP State Management (Cookies) | `Cookies/` | `CookieInjectionSpec.cs` | - -## File & Class Naming Rules - -### Old Convention (RFC-based, deprecated) - -```csharp -// File: RFC9113/01_Http2EncoderStageTests.cs -// Class: Http2EncoderStageTests -// Method: [Fact(DisplayName = "RFC9113-4.1-FRM-005: description")] -public async Task Should_SetKeyFromFrame() { } -``` - -### New Convention (component-based, post-Feature-040) - -```csharp -// File: Http2/Encoding/Http2EncoderSpec.cs -// Namespace: TurboHTTP.StreamTests.Http2.Encoding -// Class: Http2EncoderSpec : StreamTestBase -// Method: [Trait("RFC", "RFC9113-4.1")] -public sealed class Http2EncoderSpec : StreamTestBase -{ - [Fact(Timeout = 5000)] - [Trait("RFC", "RFC9113-4.1")] - public async Task Http2Encoder_should_set_key_from_frame() - { - // BDD-style method name replaces DisplayName - } -} -``` - -## Naming Conventions (Post-Feature-040) - -- **File names**: Drop numeric prefix `NN_`, use `Spec` suffix (Akka.NET convention) - - `Http2EncoderSpec.cs`, `HpackEncodingSpec.cs`, `CacheValidationSpec.cs` -- **Class names**: `Spec` suffix, `sealed` - - `public sealed class Http2EncoderSpec : StreamTestBase` -- **Method names**: BDD style `Subject_should_behavior()` or `Subject_must_behavior_when_condition()` - - `Http2Encoder_should_set_key_from_frame()` - - `Cache_must_reject_expired_entries_when_max_age_exceeded()` -- **Namespaces**: Component-based, matching folder structure - - `TurboHTTP.Tests.Http2.Encoding`, `TurboHTTP.Tests.Caching`, `TurboHTTP.Tests.Cookies` -- **RFC traceability**: Use `[Trait("RFC", "RFC-
")]` (replaces `DisplayName` RFC tags) - - `[Trait("RFC", "RFC9113-4.1")]`, `[Trait("RFC", "RFC7541-6.3")]`, `[Trait("RFC", "RFC6265-4.1")]` - - CI filter: `dotnet test --filter "Trait~RFC9113"` (tilde = contains) -- **`[Fact(DisplayName = ...)]` is deprecated** — method name IS the documentation -- **Timeouts REQUIRED**: `[Fact(Timeout = 5000)]` on all async tests or `CancellationToken` with timeout -- **`[Fact]` vs `[Theory]`**: unchanged - - `[Fact]` for single cases - - `[Theory]` + `[InlineData]` for parameterised cases -- Do NOT add `#nullable enable` at the top of test files -- **Max 500 lines per test class** — split into multiple files if exceeded - -## Migration Priority (Strangler Fig Strategy) - -The RFC-based folders are being replaced incrementally. Migration order: - -1. **Cookies (RFC 6265)** → `Cookies/` — 2-3 files (quick win) -2. **Caching (RFC 9111)** → `Caching/` — 6-8 files (quick win) -3. **Semantics (RFC 9110)** → `Semantics/` — ~17 files (opportunistic) -4. **Http10 (RFC 1945)** → `Http10/` — ~28 files (opportunistic) -5. **Http11 (RFC 9112)** → `Http11/` — ~44 files (opportunistic) -6. **Http2 + HPACK (RFC 9113 + RFC 7541)** → `Http2/` — ~36 files (Feature 40-62 Http2Decoder migration) -7. **Http3 + QPACK (RFC 9114 + RFC 9204)** → `Http3/` — ~60 files (opportunistic) - -**No big-bang sprint:** New tests land directly in the new structure; old tests migrate as they are touched. - -## Guard-Rail: spec-naming-validator - -The `spec-naming-validator` agent validates naming conventions in new component-based test files: -- Checks `Spec.cs` file names, `sealed` classes, BDD method names, `[Trait("RFC", ...)]` usage -- Does NOT block build/tests — it is a quality gate for new code -- Run after adding new test files: `spec-naming-validator` (`.claude/agents/spec-naming-validator`) diff --git a/notes/Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE.md b/notes/Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE.md deleted file mode 100644 index 1f658bfcf..000000000 --- a/notes/Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE.md +++ /dev/null @@ -1,366 +0,0 @@ ---- -title: Dispatcher Configuration Implementation Guide -date: '2026-04-03' -status: ready-to-implement -tags: - - implementation - - configuration - - akka-streams - - threading -related: - - Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS.md ---- -# Dispatcher Configuration Implementation Guide - -## Quick Reference: Akka.NET Dispatchers for TurboHTTP - -### The Problem -TurboHTTP's HTTP/2 multiplexing with 64+ concurrent requests causes .NET ThreadPool contention, leading to deadlocks in BenchmarkDotNet processes. The default dispatcher routes all actor work through the shared global ThreadPool, which also handles application async/await continuations. - -### The Solution -**Use ChannelExecutor dispatcher** (available in Akka.NET 1.5.x). - -ChannelExecutor: -- Runs on the .NET ThreadPool but dynamically scales it -- Reduces idle thread count while maintaining performance -- Proven faster than ForkJoinDispatcher in benchmarks -- Eliminates ThreadPool starvation issues -- Works well in cloud/containerized environments - ---- - -## Configuration Options - -### Option 1: Global Default (Recommended for TurboHTTP) - -Apply ChannelExecutor as the system-wide default dispatcher: - -```hocon -akka { - actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } -} -``` - -**Parameters:** -- `executor = channel-executor` — Use ChannelExecutor instead of ThreadPool -- `throughput = 30` — Process 30 messages per actor before yielding (lower = more responsive, higher = better throughput) -- `parallelism-min = 2` — Minimum thread pool threads (keep low to reduce startup overhead) -- `parallelism-factor = 2.0` — Multiply logical core count (e.g., 8 cores × 2.0 = 16 threads) -- `parallelism-max = 128` — Hard limit on threads (cap at expected max concurrent load) - ---- - -### Option 2: Production ASP.NET Core - -For applications running in ASP.NET Core with ThreadPool already in use: - -```hocon -akka { - actor { - default-dispatcher = { - executor = channel-executor - throughput = 20 # More responsive due to app code also needing ThreadPool - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 1.0 # Exactly 1x core count - parallelism-max = 64 - } - } - } -} -``` - -Reasoning: Conservative parallelism settings since ASP.NET Core also needs ThreadPool threads. - ---- - -### Option 3: High-Throughput Streaming (Benchmarks/Load Tests) - -For maximum throughput in controlled benchmarking environments: - -```hocon -akka { - actor { - default-dispatcher = { - executor = channel-executor - throughput = 50 # Higher throughput prioritized over latency - fork-join-executor { - parallelism-min = 1 - parallelism-factor = 2.0 - parallelism-max = 256 - } - } - } -} -``` - ---- - -### Option 4: ForkJoinDispatcher (If Maximum Predictability Needed) - -If you need guaranteed latency instead of dynamic scaling: - -```hocon -akka { - actor { - default-dispatcher = { - type = ForkJoinDispatcher - throughput = 30 - dedicated-thread-pool { - thread-count = 32 - deadlock-timeout = 10s - threadtype = background - } - } - } -} -``` - -**Trade-offs:** -- ✓ Eliminates ThreadPool variance entirely -- ✓ Predictable latency -- ✗ Higher memory usage (dedicated threads always running) -- ✗ Worse in cloud/containerized environments - ---- - -## Implementation Steps for TurboHTTP - -### Step 1: Update TurboClientServiceCollectionExtensions.cs - -```csharp -// File: /src/TurboHTTP/TurboClientServiceCollectionExtensions.cs - -private static readonly Config LoggingHocon = ConfigurationFactory.ParseString( - """ - akka.loggers = ["Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"] - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - -### Step 2: Update Benchmark Configuration - -```csharp -// File: /src/TurboHTTP.Benchmarks/StreamingThroughputBenchmarks.cs - -private static readonly Config BenchHocon = ConfigurationFactory.ParseString( - """ - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - -### Step 3: Run Validation Tests - -```bash -# Run benchmarks to confirm no deadlocks/hangs -dotnet run --configuration Release --project src/TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj - -# Run integration tests -dotnet test --project src/TurboHTTP.IntegrationTests/TurboHTTP.IntegrationTests.csproj - -# Run stream tests -dotnet test --project src/TurboHTTP.StreamTests/TurboHTTP.StreamTests.csproj -``` - -### Step 4: Performance Validation - -Check that: -- No timeouts or deadlocks occur -- Throughput improves (compare before/after benchmark results) -- Memory usage is reasonable -- CPU utilization is stable - ---- - -## Parameter Tuning Guide - -### throughput - -Controls how many messages an actor processes before yielding to other actors. - -``` -throughput = N # Process N messages, then yield -``` - -**Tuning:** -- `throughput = 10-20` → More responsive (fair scheduling, higher context switches) -- `throughput = 30-50` → Balanced (default sweet spot) -- `throughput = 100+` → Higher throughput (less fair, possible starvation) - -For HTTP/2: Use `30-50` for balanced latency/throughput. - -### parallelism-factor - -Multiplies logical core count to determine max threads in the pool. - -``` -parallelism-factor = 1.0 # 1x core count -parallelism-factor = 2.0 # 2x core count -``` - -**Tuning:** -- `1.0` → Conservative (one thread per core) — good for CPU-bound work -- `2.0` → Recommended for I/O-heavy (network requests) — one extra thread per core for I/O wait -- `4.0+` → Only if many blocking operations expected - -For HTTP/2: Use `2.0` (each core can handle one network I/O wait). - -### parallelism-max - -Hard limit on total threads the dispatcher can spawn. - -``` -parallelism-max = N # Never exceed N threads -``` - -**Tuning:** -- Should be `2x * logical_core_count` at minimum -- Set to expected max concurrent actors -- For 64 concurrent HTTP/2 requests: use `128-256` - ---- - -## Dispatcher Selection Decision Tree - -``` -Does your application share the .NET ThreadPool? -├─ YES (ASP.NET Core, background services, etc.) -│ └─ Use: ChannelExecutor ✓ (Option 2) -│ -└─ NO (Standalone/benchmarking) - ├─ Need maximum throughput? - │ └─ YES → Use: ChannelExecutor (Option 3) ✓ - │ - └─ Need predictable latency (< 1ms variance)? - └─ YES → Use: ForkJoinDispatcher (Option 4) - └─ NO → Use: ChannelExecutor (Option 1) ✓ -``` - ---- - -## Performance Expectations - -### Before (Default ThreadPoolDispatcher) -- ThreadPool contention under 64+ concurrent requests -- Possible deadlocks in BenchmarkDotNet -- Unpredictable latency spikes -- High context-switch overhead - -### After (ChannelExecutor) -- Minimal ThreadPool contention (dynamic scaling) -- No deadlocks -- Stable latency across all concurrency levels -- Reduced idle CPU -- 5-10% throughput improvement (proven in Akka benchmarks) - ---- - -## Monitoring the Dispatcher - -### Check Active Thread Count - -```csharp -// Get current thread count info -var stats = ThreadPool.GetAvailableThreads(out int completionThreads, out _); -Console.WriteLine($"Available: {stats}, Completion: {completionThreads}"); -``` - -### Expected Behavior with ChannelExecutor - -Under load: -- Thread count should increase dynamically -- Idle time should show significant reduction -- No starvation of application threads - -### Verify Configuration - -```csharp -// Log Akka configuration -var system = ActorSystem.Create("test"); -Console.WriteLine(system.Settings.Config); // Prints full HOCON config -``` - -Should show: -``` -akka.actor.default-dispatcher.executor = channel-executor -``` - ---- - -## Troubleshooting - -### Issue: Still seeing deadlocks - -**Causes:** -- Configuration not applied (check ActorSystem creation code) -- Blocking calls within actors (violates actor model) -- Insufficient `parallelism-max` for actual concurrency - -**Solution:** -- Verify config with `system.Settings.Config` -- Audit actor code for blocking operations (`.Result`, `.Wait()`) -- Increase `parallelism-max` if hitting the limit - -### Issue: Memory usage increased - -**Causes:** -- `parallelism-factor` too high -- `parallelism-max` exceeds available system memory - -**Solution:** -- Reduce `parallelism-factor` to 1.0 -- Lower `parallelism-max` if memory-constrained - -### Issue: Latency worse than before - -**Causes:** -- `throughput` too high (thread context switch reduced unfairly) -- ChannelExecutor dynamic scaling thrashing (scale up/down rapidly) - -**Solution:** -- Lower `throughput` to 15-20 -- Stabilize `parallelism-min` to prevent scale-thrashing - ---- - -## References - -- [[Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS|Dispatcher Selection Analysis]] — Full comparison of all dispatcher types -- [Official Akka.NET Docs](https://getakka.net/articles/actors/dispatchers.html) -- Akka.NET GitHub: https://github.com/akkadotnet/akka.net - ---- - -## Checklist: Before Committing - -- [ ] Configuration applied to ActorSystem bootstrap -- [ ] No compilation errors -- [ ] Benchmarks run without deadlocks/timeouts -- [ ] Integration tests pass -- [ ] Memory usage validated -- [ ] Throughput improved or maintained -- [ ] Documentation updated (CLAUDE.md) diff --git a/notes/Architecture/Guides/11-STAGE_PORT_NAMING.md b/notes/Architecture/Guides/11-STAGE_PORT_NAMING.md deleted file mode 100644 index a1fa42358..000000000 --- a/notes/Architecture/Guides/11-STAGE_PORT_NAMING.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Stage Inlet/Outlet Port Naming -tags: [architecture, conventions, akka-streams] -created: 2026-04-13 -updated: 2026-04-13 ---- - -# Stage Inlet/Outlet Port Naming - -All `GraphStage` inlet/outlet string names follow `StageName.Direction` or `StageName.Direction.Role` (PascalCase). C# field names mirror the same pattern. - -## Patterns by Shape Type - -| Shape Type | Inlet pattern | Outlet pattern | Example | -|-----------|--------------|----------------|---------| -| FlowShape (1 in, 1 out) | `StageName.In` | `StageName.Out` | `"Http11Encoder.In"` / `"Http11Encoder.Out"` | -| FanOutShape (1 in, 2+ out) | `StageName.In` | `StageName.Out.Role` | `"Redirect.In"` / `"Redirect.Out.Final"` | -| FanInShape (2+ in, 1 out) | `StageName.In.Role` | `StageName.Out` | `"Http20Correlation.In.Request"` / `"Http20Correlation.Out"` | -| Custom multi-port | `StageName.In.Role` | `StageName.Out.Role` | `"Http20Connection.In.Server"` / `"Http20Connection.Out.Stream"` | - -## C# Field Naming - -- Simple shapes: `_in` / `_out` -- Multi-port shapes: `_inRole` / `_outRole` - -## Rules - -- **PascalCase** for all name segments -- **No protocol prefix** (not `Http11.Http11Encoder.In`) -- **Drop `Stage` suffix** (use `Http11Encoder`, not `Http11EncoderStage`) -- **Semantic role names**: `Request`, `Response`, `Final`, `Retry`, `Redirect`, `Signal`, `Miss`, `Hit`, `Server`, `Stream`, `App` -- **Globally unique port names** across the entire codebase - -## Validation - -Use the `stage-port-validator` agent (`.claude/agents/stage-port-validator`) to scan all stages for naming violations. diff --git a/notes/Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE.md b/notes/Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE.md deleted file mode 100644 index 2941683a2..000000000 --- a/notes/Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Dispatcher Selection Quick Reference Card -date: '2026-04-03' -tags: - - dispatcher - - reference - - quick-lookup ---- -# Dispatcher Quick Reference Card - -## TL;DR: Choose ChannelExecutor - -For TurboHTTP's HTTP/2 pipeline with 64+ concurrent requests: - -```hocon -akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } -} -``` - -Done. This solves ThreadPool contention, beats other dispatchers on performance, and requires zero API changes. - ---- - -## All Dispatcher Types at a Glance - -### ThreadPoolDispatcher (DEFAULT) -- Uses: Global .NET ThreadPool -- Problem: Competes with app code -- Throughput: 4,800 req/s -- Best for: Light workloads only - -### ForkJoinDispatcher -- Uses: Dedicated thread pool (32 threads) -- Advantage: No ThreadPool competition -- Problem: Higher memory, idle CPU -- Throughput: 5,100 req/s -- Best for: Latency-critical workloads with memory budget - -### ChannelExecutor ← USE THIS -- Uses: ThreadPool + dynamic scaling -- Advantage: No contention, low memory, fast -- Throughput: 5,200+ req/s (fastest) -- Best for: High-throughput streaming, HTTP/2 - -### PinnedDispatcher -- Uses: One thread per actor -- Problem: Too many threads for 64 concurrent requests -- Best for: Never (except very rare edge cases) - -### SynchronizedDispatcher -- Uses: SynchronizationContext -- Problem: Not for backend services -- Best for: WinForms/WPF UI only - -### TaskDispatcher -- Uses: TPL (same as default) -- Problem: No advantage over default -- Best for: Obsolete in .NET 10 - ---- - -## Why ChannelExecutor - -Problem: 64 concurrent requests × Akka actors × network I/O all compete for ThreadPool -→ Deadlock - -Solution: Use internal channel queue + dynamic ThreadPool scaling -→ No contention, no deadlock, better performance - ---- - -## Configuration Comparison - -### Minimum (Development) -```hocon -executor = channel-executor -parallelism-max = 32 -throughput = 20 -``` - -### Balanced (Default for TurboHTTP) -```hocon -executor = channel-executor -parallelism-factor = 2.0 -parallelism-max = 128 -throughput = 30 -``` - -### Maximum Throughput (Benchmarks) -```hocon -executor = channel-executor -parallelism-factor = 2.0 -parallelism-max = 256 -throughput = 50 -``` - -### If You Must Have Latency Guarantees -```hocon -type = ForkJoinDispatcher -dedicated-thread-pool { - thread-count = 32 - deadlock-timeout = 10s -} -throughput = 30 -``` - ---- - -## Parameter Meanings - -| Parameter | Meaning | Range | Default | -|-----------|---------|-------|---------| -| `executor` | Which executor type | `channel-executor`, `ForkJoinDispatcher` | none | -| `throughput` | Messages processed before context switch | 1-1000 | 30 | -| `parallelism-min` | Minimum threads | 1+ | 2 | -| `parallelism-factor` | Multiply core count | 0.1-4.0 | 2.0 | -| `parallelism-max` | Hard thread limit | 1+ | 128 | - ---- - -## Decision Tree: Which Dispatcher? - -``` -Is this TurboHTTP HTTP/2 streaming? -├─ YES → ChannelExecutor ✓ -└─ NO - ├─ Need low latency variance (<1ms)? - │ ├─ YES + memory available → ForkJoinDispatcher - │ └─ NO → ChannelExecutor ✓ - │ - └─ Is this a UI app? - ├─ YES → SynchronizedDispatcher - └─ NO → ChannelExecutor ✓ (default for everything else) -``` - ---- - -## Performance Comparison - -| Scenario | Default | ForkJoin | ChannelExecutor | -|----------|---------|----------|-----------------| -| 1 request | 96 μs | 100 μs | 99 μs | -| 64 concurrent | STALLS | 169 μs | 169 μs ← Best | -| 256 concurrent | DEADLOCK | 190 μs | 170 μs ← Best | -| Memory | Low | High | Low ← Best | -| Idle CPU | Baseline | Constant | Dynamic ← Best | - ---- - -## Implementation Checklist - -``` -[ ] Add ChannelExecutor config to LoggingHocon -[ ] Add ChannelExecutor config to BenchHocon -[ ] Run: dotnet build -[ ] Run: dotnet test --project TurboHTTP.Tests -[ ] Run: dotnet run --project TurboHTTP.Benchmarks -[ ] Verify: No deadlocks, timeouts, hangs -[ ] Done! -``` - ---- - -## Common Tuning Scenarios - -### "Too much idle CPU, reduce memory" -``` -Reduce: parallelism-factor from 2.0 to 1.0 -Result: Fewer threads, less idle CPU -``` - -### "Latency is spiking" -``` -Check: throughput too high (50+)? -Try: Reduce throughput to 20-30 -Or: Increase parallelism-max to 256 -``` - -### "Still seeing contention" -``` -Check: parallelism-max too low? -Try: Increase to 256 (allow more dynamic scaling) -``` - -### "Memory usage too high" -``` -Check: parallelism-factor too high? -Try: Reduce from 2.0 to 1.0 -Also: Lower parallelism-max from 128 to 64 -``` - ---- - -## Verify Configuration is Applied - -```csharp -var system = ActorSystem.Create("test"); -Console.WriteLine(system.Settings.Config); -// Should contain: executor = channel-executor -``` - ---- - -## File Locations - -- **Main config:** `/src/TurboHTTP/TurboClientServiceCollectionExtensions.cs` (LoggingHocon) -- **Benchmark config:** `/src/TurboHTTP.Benchmarks/StreamingThroughputBenchmarks.cs` (BenchHocon) -- **Test config:** `/src/TurboHTTP.IntegrationTests/Shared/ActorSystemFixture.cs` (optional) - ---- - -## Links - -- Full Analysis: [[Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS|Dispatcher Selection Analysis]] -- Implementation Guide: [[Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE|Dispatcher Configuration Guide]] -- Status Report: [[Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION|ThreadPool Contention Resolution]] -- Official Docs: https://getakka.net/articles/actors/dispatchers.html - ---- - -## Bottom Line - -**Use ChannelExecutor. It solves the problem. Ship it.** diff --git a/notes/Architecture/Guides/12-OBSIDIAN_WORKFLOW.md b/notes/Architecture/Guides/12-OBSIDIAN_WORKFLOW.md deleted file mode 100644 index d25afc3da..000000000 --- a/notes/Architecture/Guides/12-OBSIDIAN_WORKFLOW.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Obsidian Vault Workflow -tags: [architecture, workflow, knowledge-management] -created: 2026-04-13 -updated: 2026-04-13 ---- - -# Obsidian Vault Workflow - -The project knowledge base lives in `notes/` as an Obsidian vault. This is the single source of truth for all non-code knowledge. - -## Access Rules - -- **ALWAYS use Obsidian MCP tools** (`search_notes`, `read_note`, `write_note`, `patch_note`, etc.) to interact with the vault — NEVER use `Read`/`Write`/`Edit` file tools on `notes/` files -- MCP ensures Obsidian indexes stay consistent and frontmatter is properly handled - -## When to READ from Obsidian - -- Before working on any RFC-related task → `search_notes("RFC XXXX section Y")` -- Before architecture decisions → `search_notes("component name")` -- When you don't know something about the project → search the vault first -- When investigating bugs → check `notes/Debugging/` and `notes/Architecture/` -- Before implementing features → check `notes/Features/` - -## When to WRITE to Obsidian - -| Discovery Type | Destination | MCP Action | -|----------------|-------------|------------| -| RFC compliance gaps | `RFC/` | `write_note` with RFC-Note template structure | -| Architecture decisions | `Architecture/` | `write_note` with ADR template structure | -| Protocol limitations | `Architecture/` | `write_note` or `patch_note` | -| Bug investigations | `Debugging/` | `write_note` with Bug-Investigation structure | -| Feature learnings | `Features/` | `write_note` | -| Benchmark findings | `Architecture/` | `patch_note` on existing benchmark note | - -**Before ending any session**: Check — did I discover something important? If yes → `write_note` or `patch_note` in Obsidian. - -## Vault Structure - -``` -notes/ -├── 00-Index.md # Central hub — START HERE -├── Architecture/ # ADRs, design decisions, patterns, preferences, limitations -│ ├── Analysis/ # Deep-dive analysis notes -│ ├── Design/ # Core architecture documents -│ ├── Guides/ # How-to guides and conventions -│ └── Status/ # Project status tracking -├── RFC/ # Per-RFC compliance tracking (with sections/ subfolders) -├── rfc/ # RFC reference documents (quick refs, analysis) -├── Features/ # Feature plans and progress -│ ├── Diagnostics/ -│ ├── Infrastructure/ -│ ├── Performance/ -│ ├── Protocol/ -│ └── Testing/ -├── Templates/ # Session-Log, RFC-Note, ADR, Bug-Investigation -└── Debugging/ # (git-ignored) Bug investigations -``` - -## Key Notes Reference - -- [[01-LAYERED_ARCHITECTURE]] — Full layer-by-layer architecture -- [[02-STAGE_PATTERNS]] — GraphStage patterns and conventions -- [[04-CURRENT_STATE_SUMMARY]] — Project status, completeness scores -- [[05-BENCHMARK_PATTERNS]] — BDN conventions, port assignments, TCP workarounds -- [[06-DECODER_PIPELINE_ARCHITECTURE]] — Three-layer decoder pattern -- [[09-CLAUDE_PREFERENCES]] — Language, workflow, response style preferences -- [[Architecture/Guides/10-TEST_CONVENTIONS|Test Conventions]] — Test naming, structure, migration strategy -- [[Architecture/Guides/11-STAGE_PORT_NAMING|Stage Port Naming]] — Inlet/outlet port naming reference diff --git a/notes/Architecture/Guides/12-TEST_ORGANIZATION.md b/notes/Architecture/Guides/12-TEST_ORGANIZATION.md deleted file mode 100644 index 17586df2a..000000000 --- a/notes/Architecture/Guides/12-TEST_ORGANIZATION.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Test Organization & Infrastructure -description: >- - Test project structure, base classes, integration fixtures, folder mapping, - and conventions -tags: - - testing - - infrastructure - - conventions - - xunit -aliases: - - Test Structure - - Test Infrastructure - - Testing Guide ---- -# Test Organization & Infrastructure - -**Last Updated**: 2026-04-07 - -## Test Projects - -| Project | Purpose | Count | -|---------|---------|-------| -| `src/TurboHTTP.Tests/` | Unit tests organized by component/protocol version | 260+ | -| `src/TurboHTTP.StreamTests/` | Akka.Streams stage behavior tests | — | -| `src/TurboHTTP.IntegrationTests/` | End-to-end tests with Kestrel | 515+ | -| `src/TurboHTTP.Benchmarks/` | BenchmarkDotNet performance tests | 25+ | - -## Unit Tests (`TurboHTTP.Tests/`) - -Organized by component/protocol version (post-Feature-040): - -| Folder | Component | RFC | Example Files | -|--------|-----------|-----|----------------| -| `Http10/` | HTTP/1.0 | RFC 1945 | `Http10EncoderSpec.cs`, `Http10ParserSpec.cs` | -| `Http11/` | HTTP/1.1 | RFC 9112 | `Http11EncoderSpec.cs`, `Http11ChunkedDecoderSpec.cs` | -| `Http11/Encoding/` | HTTP/1.1 Encoding | RFC 9112 | `Http11EncoderSpec.cs` | -| `Http11/Decoding/` | HTTP/1.1 Decoding | RFC 9112 | `Http11DecoderSpec.cs` | -| `Http11/Chunking/` | HTTP/1.1 Chunked Transfer | RFC 9112 | `Http11ChunkedDecoderSpec.cs` | -| `Http2/` | HTTP/2 Frames & Streams | RFC 9113 | `Http2FrameDecoderSpec.cs`, `Http2ConnectionSpec.cs` | -| `Http2/Frames/` | HTTP/2 Frame Layer | RFC 9113 | `Http2FrameDecoderSpec.cs` | -| `Http2/Connection/` | HTTP/2 Connection | RFC 9113 | `Http2ConnectionSpec.cs` | -| `Http2/Stream/` | HTTP/2 Stream | RFC 9113 | `Http2StreamSpec.cs` | -| `Http2/Hpack/` | HPACK Header Compression | RFC 7541 | `HpackEncodingSpec.cs`, `HpackDecodingSpec.cs` | -| `Http3/` | HTTP/3 (QUIC) | RFC 9114 | `Http3ConnectionSpec.cs`, `Http3FrameDecoderSpec.cs` | -| `Http3/Frames/` | HTTP/3 Frame Layer | RFC 9114 | `Http3FrameDecoderSpec.cs` | -| `Http3/Connection/` | HTTP/3 Connection | RFC 9114 | `Http3ConnectionSpec.cs` | -| `Http3/Qpack/` | QPACK Header Compression | RFC 9204 | `QpackEncodingSpec.cs`, `QpackDecodingSpec.cs` | -| `Semantics/` | HTTP Semantics | RFC 9110 | `RedirectHandlingSpec.cs`, `RetryPolicySpec.cs` | -| `Caching/` | HTTP Caching | RFC 9111 | `CacheValidationSpec.cs`, `CacheStorageSpec.cs` | -| `Cookies/` | HTTP State Management | RFC 6265 | `CookieInjectionSpec.cs`, `CookieStorageSpec.cs` | -| `Transport/` | Connection pooling & management | — | `ConnectionPoolSpec.cs`, `LeaseManagementSpec.cs` | -| `Security/` | TLS, certificate validation | — | `CertificateValidationSpec.cs` | -| `Diagnostics/` | Telemetry & logging | — | `LoggingSpec.cs`, `TraceContextSpec.cs` | -| `Hosting/` | Client builder & DI | — | `ClientBuilderSpec.cs`, `HostingExtensionsSpec.cs` | - -**File naming**: `Spec.cs` — descriptive name with `Spec` suffix (Akka.NET convention). Numeric prefixes (`NN_`) are deprecated. - -## Stream Tests (`TurboHTTP.StreamTests/`) - -Tests Akka.Streams GraphStage behavior. Organized by component (mirroring `TurboHTTP.Tests`): - -| Folder | Coverage | -|--------|----------| -| `Http10/` | HTTP/1.0 encoder/decoder/roundtrip stages, TCP fragmentation | -| `Http11/` | HTTP/1.1 encoder/decoder/chunked/correlation/pipeline/connection stages | -| `Http2/Frames/` | HTTP/2 frame encoding/decoding stages | -| `Http2/Connection/` | HTTP/2 connection management stages | -| `Http2/Stream/` | HTTP/2 stream lifecycle stages | -| `Http2/Hpack/` | HPACK encoder/decoder stream integration | -| `Http3/Frames/` | HTTP/3 frame encoding/decoding stages | -| `Http3/Connection/` | HTTP/3 connection management stages | -| `Http3/Qpack/` | QPACK encoder/decoder stream integration | -| `Semantics/` | Decompression, redirect, retry stage tests | -| `Caching/` | Cache lookup and storage stage tests | -| `Cookies/` | Cookie injection and storage stage tests | -| `Streams/` | Stage infrastructure: connection, engine routing, enricher, buffer lifecycle, pipeline wiring | -| `IO/` | ConnectionActor, HostPool, ConnectionState, ConnectionHandle, ClientByteMover, ClientRunner, QUIC tests | - -**File naming**: Component-based folder files use descriptive names with `Spec` suffix (`Http11EncoderSpec.cs`, `HpackEncodingSpec.cs`); `Streams/` and `IO/` use numeric prefix for ordered tests. - -## Base Classes - -### StreamTestBase -- Extends `TestKit` (Akka.TestKit.Xunit) -- Creates `IMaterializer` for test-scoped stream materialization -- Used by all stream tests in `TurboHTTP.StreamTests/` - -### EngineTestBase -- Full engine round-trip helper -- Builds complete protocol engine graphs for integration-style stream tests -- Provides helper methods for encoding requests and decoding responses through the full pipeline - -### IOActorTestBase -- Actor lifecycle tests in `TurboHTTP.StreamTests/IO/` -- Tests connection actors, host pools, and transport-level behavior - -## Integration Test Fixtures - -Kestrel-based fixtures for end-to-end HTTP testing: - -| Fixture | Protocol | Purpose | -|---------|----------|---------| -| `KestrelFixture` | HTTP/1.1 (plaintext) | Standard HTTP/1.1 testing | -| `KestrelH2Fixture` | HTTP/2 (TLS) | HTTP/2 over HTTPS testing | -| `KestrelH3Fixture` | HTTP/3 (QUIC) | HTTP/3 over QUIC testing | -| `KestrelTlsFixture` | HTTP/1.1 (TLS) | TLS/HTTPS testing | - -- **60+ routes** registered across fixtures -- **SmokeTests.cs** provides initial end-to-end coverage -- Each fixture starts a real Kestrel server with dynamic port discovery - -## Conventions (Post-Feature-040) - -- **Max 500 lines** per test class — split into multiple focused files if exceeded -- **Timeout REQUIRED** on all async tests: `[Fact(Timeout = 5000)]` or `CancellationToken` -- **RFC Traceability**: Use `[Trait("RFC", "RFC-
")]` instead of `DisplayName` (e.g., `[Trait("RFC", "RFC9113-4.1")]`) -- **Method names**: BDD style `Subject_should_behavior()` (e.g., `Http2Encoder_should_set_key_from_frame()`) -- **Sealed classes**: `public sealed class` for all test classes -- **Namespace**: matches component folder (e.g., `namespace TurboHTTP.Tests.Http2;` or `TurboHTTP.Tests.Http2.Encoding;`) -- **File naming**: `Spec.cs` with `Spec` suffix (Akka.NET convention) -- **No `#nullable enable`**: enabled at project level - -## Completed Testing Phases - -| Phase | Description | Result | -|-------|-------------|--------| -| 1-10 | RFC Compliance (HTTP/1.0, 1.1, 2.0, HPACK) | 260+ unit tests | -| 11 | Core Benchmarks | 26 benchmarks | -| 12-17 | Integration Tests | 515+ tests (real TCP + Kestrel) | -| 18 | Core Performance Validation | 15 benchmarks | -| 19 | Streaming & Protocol Efficiency | 14 benchmarks | -| 20 | Concurrency & Production Load Simulation | 16 benchmarks | -| 21 | Enterprise Stability & Real World Patterns | 21 benchmarks | -| 22 | Release Throughput Validation | 2 benchmarks | -| 39 | Http2Decoder deprecation | ✅ Marked [Obsolete], 509 warnings, 0 errors | diff --git a/notes/Architecture/Guides/17-DIAGNOSTICS_INTEGRATION.md b/notes/Architecture/Guides/17-DIAGNOSTICS_INTEGRATION.md deleted file mode 100644 index 2fefec742..000000000 --- a/notes/Architecture/Guides/17-DIAGNOSTICS_INTEGRATION.md +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: Diagnostics Integration Architecture -description: >- - Three-pillar observability: DiagnosticListener events, ETW EventSource, and - OpenTelemetry-compatible Metrics for TurboHTTP -tags: - - architecture - - diagnostics - - observability - - telemetry - - metrics ---- -# Diagnostics Integration Architecture - -## Purpose - -TurboHTTP provides a three-pillar observability model that integrates with standard .NET diagnostic infrastructure. All telemetry is opt-in — zero overhead when no listeners are attached. The three pillars are: - -1. **`DiagnosticListener`** — Rich structured events for distributed tracing and APM tools -2. **`EventSource` (ETW)** — Lightweight keyword-filtered events for production logging and PerfView -3. **`System.Diagnostics.Metrics`** — OpenTelemetry-compatible counters, histograms, and gauges - -> **Extends, does not repeat**: For how tracing integrates with the pipeline, see [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] (TracingBidiStage is the outermost BidiFlow). For deadlock watchdog diagnostics in DEBUG builds, see [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]]. - ---- - -## Key Files - -| Component | Path | Role | -|-----------|------|------| -| DiagnosticListener | `Diagnostics/TurboHttpDiagnosticListener.cs` | Structured event source for APM/tracing integration | -| EventSource (ETW) | `Diagnostics/TurboHttpEventSource.cs` | ETW events with keyword filtering for production logging | -| Metrics | `Diagnostics/TurboHttpMetrics.cs` | OTel-compatible counters, histograms, gauges | -| TracingBidiStage | `Streams/Stages/Features/TracingBidiStage.cs` | Pipeline stage that creates `Activity` spans per request | -| DeadlockWatchdogStage | `Streams/Stages/Routing/DeadlockWatchdogStage.cs` | DEBUG-only stage emitting stall diagnostics | - ---- - -## Data Flow - -```text -┌──────────────────────────────────────────────────────────────┐ -│ TurboHTTP Pipeline │ -│ │ -│ TracingBidiStage ◄──── Creates Activity per request │ -│ │ │ -│ ▼ │ -│ Feature BidiStages ──► Emit events at key decision points │ -│ │ │ -│ ▼ │ -│ Protocol Core ────────► Emit events on connect/disconnect │ -│ │ │ -│ ▼ │ -│ Transport Layer ──────► Emit events on socket open/close │ -└──────┬──────────┬──────────┬─────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌──────────┐ ┌──────────┐ ┌──────────────┐ -│Diagnostic│ │ ETW │ │ Metrics │ -│ Listener │ │EventSrc │ │ (OTel) │ -│ │ │ │ │ │ -│ APM/DT │ │ PerfView │ │ Prometheus │ -│ Zipkin │ │ dotnet- │ │ Grafana │ -│ Jaeger │ │ trace │ │ Azure Mon. │ -└──────────┘ └──────────┘ └──────────────┘ -``` - ---- - -## Pillar 1: DiagnosticListener - -`TurboHttpDiagnosticListener` is a static class exposing a single `DiagnosticListener` named `"TurboHTTP"`. - -### Events - -| Event Name | Payload | Emitted By | -|------------|---------|------------| -| `TurboHTTP.Request.Start` | `HttpRequestMessage` | TracingBidiStage (request direction) | -| `TurboHTTP.Request.Stop` | `HttpResponseMessage` | TracingBidiStage (response direction) | -| `TurboHTTP.Request.Failed` | `Exception` | TracingBidiStage (on upstream failure) | -| `TurboHTTP.Connection.Opened` | `RequestEndpoint` | ConnectionStage (on connect) | -| `TurboHTTP.Connection.Closed` | `RequestEndpoint, CloseKind` | ConnectionStage (on disconnect) | -| `TurboHTTP.DeadlockStall` | `StageName, Duration` | DeadlockWatchdogStage (DEBUG only) | - -### Guard Pattern - -All event emission is guarded by `IsEnabled()` checks to avoid payload allocation when no subscriber is attached: - -```csharp -if (Source.IsEnabled("TurboHTTP.Request.Start")) -{ - Source.Write("TurboHTTP.Request.Start", new { Request = request }); -} -``` - -This ensures **zero allocation overhead** when diagnostics are not subscribed. - -### Subscribing - -```csharp -DiagnosticListener.AllListeners.Subscribe(listener => -{ - if (listener.Name == "TurboHTTP") - { - listener.Subscribe(kvp => - { - // Handle events by kvp.Key - }); - } -}); -``` - -### Activity Integration - -`TracingBidiStage` creates a root `Activity` named `"TurboHTTP.Request"` for each request passing through the pipeline. The activity: -- Starts on request entry (outermost BidiStage, request direction) -- Tags with `http.method`, `http.url`, `http.version` -- Stops on response exit (outermost BidiStage, response direction) -- Sets `ActivityStatusCode.Error` on failure - -This integrates with `System.Diagnostics.ActivitySource` for W3C Trace Context propagation. - ---- - -## Pillar 2: EventSource (ETW) - -`TurboHttpEventSource` is an ETW `EventSource` singleton (`TurboHttpEventSource.Log`) providing keyword-filtered events for production environments. - -### Keyword Groups - -| Keyword | Value | Events | Use Case | -|---------|-------|--------|----------| -| Connection | 0x01 | ConnectionOpened (1), ConnectionClosed (2) | Connection lifecycle monitoring | -| Request | 0x02 | RequestStart (3), RequestStop (4), RequestFailed (5) | Request-level tracing | -| Protocol | 0x04 | ProtocolNegotiated (6), ProtocolError (7), SettingsReceived (8) | Protocol debugging | -| Cache | 0x08 | CacheHit (9), CacheMiss (10) | Cache effectiveness analysis | -| Retry | 0x10 | RetryAttempt (11), RedirectFollowed (12) | Retry/redirect monitoring | - -### Event Levels - -- **Informational**: Normal lifecycle events (connect, request start/stop, cache hit) -- **Warning**: Retry attempts, redirects, protocol negotiation fallbacks -- **Error**: Request failures, protocol errors, connection failures - -### Usage with dotnet-trace - -```bash -dotnet-trace collect --providers TurboHTTP:0x1F:4 -# name keywords level(Informational) -``` - -### Usage with PerfView - -```text -PerfView /providers=TurboHTTP:0x1F:4 collect -``` - ---- - -## Pillar 3: Metrics (OpenTelemetry-Compatible) - -`TurboHttpMetrics` exposes a static `Meter` named `"TurboHTTP"` with instruments following OpenTelemetry semantic conventions. - -### Instruments - -| Instrument | Type | Unit | Description | -|------------|------|------|-------------| -| `turbohttp.request.count` | Counter | `{request}` | Total requests sent | -| `turbohttp.request.duration` | Histogram | `ms` | Request round-trip duration | -| `turbohttp.cache.hit` | Counter | `{hit}` | Cache hit count | -| `turbohttp.cache.miss` | Counter | `{miss}` | Cache miss count | -| `turbohttp.retry.count` | Counter | `{retry}` | Retry attempt count | -| `turbohttp.redirect.count` | Counter | `{redirect}` | Redirect follow count | -| `turbohttp.connection.duration` | Histogram | `ms` | Connection lifetime duration | -| `turbohttp.connection.active` | UpDownCounter | `{connection}` | Currently active connections | -| `turbohttp.connection.idle` | UpDownCounter | `{connection}` | Currently idle connections | - -### Tags/Dimensions - -Metrics are tagged with: -- `http.method` — GET, POST, etc. -- `http.status_code` — Response status code -- `http.version` — 1.0, 1.1, 2, 3 -- `server.address` — Target host - -### Integration with OTel Collector - -```csharp -builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddMeter("TurboHTTP"); // Subscribe to all TurboHTTP instruments - }); -``` - -### Integration with Prometheus - -```csharp -builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddMeter("TurboHTTP"); - metrics.AddPrometheusExporter(); - }); -``` - ---- - -## Design Decisions - -1. **Three independent pillars** — Each diagnostic channel serves a different audience: DiagnosticListener for APM tools, EventSource for ops/production logging, Metrics for dashboards. They can be enabled independently with no cross-dependencies. - -2. **Zero overhead when unsubscribed** — All three pillars use guard checks (`IsEnabled()`, keyword filtering, `Meter` listener registration) to avoid allocations when no consumer is attached. This is critical for a library that sits on the hot path of every HTTP request. - -3. **TracingBidiStage as outermost layer** — Placing tracing at the outermost position in the BidiFlow chain ensures that the `Activity` span captures the full request lifecycle including retries, redirects, and cache lookups — not just the protocol-level round-trip. - -4. **Static singletons** — `TurboHttpEventSource.Log` and `TurboHttpDiagnosticListener.Source` are static singletons. This matches .NET conventions and avoids per-client diagnostic overhead. Metrics use a static `Meter` for the same reason. - -5. **DEBUG-only watchdog** — `DeadlockWatchdogStage` is conditionally compiled (`#if DEBUG`) to avoid production overhead. It emits `TurboHTTP.DeadlockStall` events when a stage stalls beyond a configurable threshold, aiding development-time deadlock detection. - ---- - -## Known Limitations - -- **No per-client Activity source** — All clients share one `ActivitySource`. If multiple `ITurboHttpClient` instances are used, traces are differentiated only by tags, not by source. This matches `HttpClient`'s behaviour. -- **No custom baggage propagation** — W3C Trace Context headers are propagated, but custom baggage items are not automatically injected into outgoing requests. Applications must add baggage manually via handlers. -- **EventSource event IDs are sequential** — Adding new events requires appending to the end of the class to maintain stable event IDs. Inserting events mid-sequence would break existing ETW consumers. -- **Histogram bucket boundaries** — Request duration and connection duration histograms use default OTel bucket boundaries. Applications with specific SLA requirements may need to configure custom boundaries via the OTel SDK. - ---- - -## Integration Points - -| Boundary | Direction | Contract | -|----------|-----------|----------| -| TracingBidiStage → DiagnosticListener | Outbound | `Request.Start/Stop/Failed` events | -| ConnectionStage → DiagnosticListener | Outbound | `Connection.Opened/Closed` events | -| DeadlockWatchdogStage → DiagnosticListener | Outbound | `DeadlockStall` events (DEBUG) | -| Feature BidiStages → EventSource | Outbound | Cache/Retry/Redirect keyword events | -| ConnectionStage → EventSource | Outbound | Connection keyword events | -| Protocol stages → EventSource | Outbound | Protocol keyword events | -| All stages → Metrics | Outbound | Counter/histogram recordings | -| External APM → DiagnosticListener | Inbound | `AllListeners.Subscribe()` | -| External OTel → Metrics | Inbound | `AddMeter("TurboHTTP")` | -| External ETW → EventSource | Inbound | Provider name + keyword mask | - ---- - -## See Also - -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — TracingBidiStage placement and DeadlockWatchdogStage -- [[Architecture/Layers/13-CLIENT_LAYER|Client Layer]] — Where diagnostic configuration is set up -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Connection events originate here -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Overall system context -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Stage lifecycle and port conventions diff --git a/notes/Architecture/Guides/_INDEX.md b/notes/Architecture/Guides/_INDEX.md deleted file mode 100644 index b8bf2530e..000000000 --- a/notes/Architecture/Guides/_INDEX.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Guides Index -description: >- - Index of convention and workflow guides — benchmarks, testing, diagnostics, - preferences -tags: - - architecture - - guides - - index ---- -# Guides - -Conventions, workflows, and reference guides for working with TurboHTTP. - -## Notes - -- [[Architecture/Guides/05-BENCHMARK_PATTERNS|Benchmark Patterns & Infrastructure]] — BenchmarkDotNet conventions, port assignments, Windows TCP TIME_WAIT workarounds -- [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Code Preferences & Workflow Guidelines]] — Language, documentation style, knowledge capture workflow, and response format -- [[Architecture/Guides/12-TEST_ORGANIZATION|Test Organization & Infrastructure]] — Test project structure, base classes, integration fixtures, folder mapping, and conventions -- [[Architecture/Guides/10-TEST_CONVENTIONS|Test Conventions]] — BDD naming, Spec suffix, Trait-based RFC traceability, component-based folder structure -- [[Architecture/Guides/11-STAGE_PORT_NAMING|Stage Port Naming]] — PascalCase port naming convention, shape patterns, global uniqueness rules -- [[Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE|Dispatcher Configuration Guide]] — ChannelExecutor configuration options, parameter tuning, and implementation steps -- [[Architecture/Guides/12-DISPATCHER_QUICK_REFERENCE|Dispatcher Quick Reference]] — One-page decision tree and configuration templates for Akka.NET dispatchers -- [[Architecture/Guides/12-OBSIDIAN_WORKFLOW|Obsidian Workflow]] — Vault conventions, MCP tool usage, and knowledge capture workflow -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION|Diagnostics Integration Architecture]] — DiagnosticListener events, ETW EventSource, and OpenTelemetry-compatible Metrics diff --git a/notes/Architecture/Layers/13-CLIENT_LAYER.md b/notes/Architecture/Layers/13-CLIENT_LAYER.md deleted file mode 100644 index 36f7ca998..000000000 --- a/notes/Architecture/Layers/13-CLIENT_LAYER.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Client Layer -description: >- - Public API surface, factory pattern, DI integration, and request lifecycle for - TurboHTTP client layer -tags: - - architecture - - client - - api - - dependency-injection ---- -# Client Layer - -The Client Layer is TurboHTTP's public API surface — the entry point for consumers who want to send HTTP requests. It follows the `HttpClientFactory` pattern from `Microsoft.Extensions.Http`, providing named/typed client instances with DI-friendly configuration. - -> **Scope**: This note covers the client-facing types only. For the internal pipeline that executes requests, see [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]]. - -## Purpose - -- Provide a familiar, `HttpClient`-compatible API for sending HTTP requests -- Support named and typed clients via `ITurboHttpClientFactory` -- Integrate with `Microsoft.Extensions.DependencyInjection` via `ITurboHttpClientBuilder` -- Allow per-client configuration of policies (redirect, retry, cache, cookies, compression) - -## Key Files - -| File | Purpose | -|------|---------| -| `src/TurboHTTP/ITurboHttpClientFactory.cs` | Factory interface — creates named `ITurboHttpClient` instances | -| `src/TurboHTTP/ITurboHttpClientBuilder.cs` | Builder interface — configures a named client's `IServiceCollection` | -| `src/TurboHTTP/TurboClientOptions.cs` | Per-client configuration: timeouts, TLS, certificates, max frame size | -| `src/TurboHTTP/TurboRequestOptions.cs` | Per-request defaults: base address, headers, version, timeout | -| `src/TurboHTTP/TurboHandler.cs` | User middleware — injected into the BidiFlow pipeline | -| `src/TurboHTTP/Streams/PipelineDescriptor.cs` | Aggregates all policies into a single record for pipeline construction | - -## Data Flow - -```text -Application Code - │ - ▼ -ITurboHttpClientFactory.CreateClient("name") - │ - ▼ -ITurboHttpClient.SendAsync(HttpRequestMessage) - │ - ▼ -Engine.CreateFlow(pool, options, descriptor) - │ - ▼ -┌──────────────────────────────────────────┐ -│ Feature BidiFlow Chain (outermost→in): │ -│ Tracing → Handlers → Redirect → Cookie │ -│ → Retry → Expect100 → Cache → Content │ -│ Encoding → Protocol Engine Core │ -└──────────────────────────────────────────┘ - │ - ▼ -HttpResponseMessage returned to caller -``` - -## Design Decisions - -### Factory Pattern over Direct Instantiation - -TurboHTTP uses `ITurboHttpClientFactory` rather than exposing constructors directly. This enables: -- **Named clients** with different configurations (e.g., "github-api" vs "internal-service") -- **Lifetime management** — the factory controls `ConnectionPool` sharing across clients -- **DI integration** — `ITurboHttpClientBuilder` plugs into `IServiceCollection` for clean startup code - -### PipelineDescriptor as Policy Aggregator - -Rather than passing 8+ policy parameters individually through the pipeline construction chain, `PipelineDescriptor` collects all optional policies into a single immutable record: - -```csharp -internal sealed record PipelineDescriptor( - RedirectPolicy? RedirectPolicy, - RetryPolicy? RetryPolicy, - Expect100Policy? Expect100Policy, - RequestCompressionPolicy? RequestCompressionPolicy, - CookieJar? CookieJar, - CacheStore? CacheStore, - CachePolicy? CachePolicy, - IReadOnlyList Handlers, - bool AutomaticDecompression = true); -``` - -Null policies are simply skipped — no BidiStage is inserted for unused features. - -### TurboHandler as BidiFlow Middleware - -User-provided `TurboHandler` instances are wrapped in `HandlerBidiStage` and stacked via `Atop` in the feature BidiFlow chain. Handlers[0] is outermost (sees initial request first, final response last). This gives middleware the same request/response interception pattern as `DelegatingHandler` in `HttpClient` but implemented as Akka.Streams BidiFlows. - -## Known Limitations - -- **No `HttpClient` drop-in replacement** — `ITurboHttpClient` is a separate interface, not a subclass of `HttpClient` -- **No automatic `HttpMessageHandler` compatibility** — existing `DelegatingHandler` chains cannot be reused directly; they must be ported to `TurboHandler` -- **Client/Handlers/Hosting directories** referenced in CLAUDE.md do not exist as separate folders yet — the types live at the project root and in `Streams/` - -## Integration Points - -| Component | Interaction | -|-----------|-------------| -| [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] | `Engine.CreateFlow()` builds the Akka.Streams pipeline from `PipelineDescriptor` | -| [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] | `ConnectionPool` is shared across clients created by the same factory | -| [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION|Diagnostics]] | `TracingBidiStage` wraps outermost layer for `Activity`-based tracing | -| `Microsoft.Extensions.DependencyInjection` | `ITurboHttpClientBuilder.Services` enables DI registration | - -## See Also - -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Where the Client Layer fits in the overall stack -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — Pipeline construction details -- [[Architecture/Guides/09-CLAUDE_PREFERENCES|Claude Preferences]] — Workflow and response conventions diff --git a/notes/Architecture/Layers/14-TRANSPORT_LAYER.md b/notes/Architecture/Layers/14-TRANSPORT_LAYER.md deleted file mode 100644 index eda282ad3..000000000 --- a/notes/Architecture/Layers/14-TRANSPORT_LAYER.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Transport Layer -description: >- - Actor-free connection pool, Channels-based I/O, TCP/TLS/QUIC transport, and - backpressure model -tags: - - architecture - - transport - - connection-pool - - channels - - tcp - - quic ---- -# Transport Layer - -The Transport Layer manages physical network connections — TCP sockets, TLS streams, and QUIC endpoints. It is **actor-free by design**: connection lifecycle is managed through `System.Threading.Channels` and `System.IO.Pipelines` instead of Akka actors, reducing overhead and simplifying the concurrency model. - -> **Scope**: This note covers connection management and byte-level I/O. For protocol framing (HTTP/1.x, HTTP/2, HTTP/3), see [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]]. - -## Purpose - -- Establish and manage per-host TCP/TLS/QUIC connections -- Provide version-aware connection pooling (HTTP/1.0 no-reuse, HTTP/1.1 keep-alive, HTTP/2 multiplexing) -- Bridge Akka.Streams `GraphStage` I/O with raw network streams via `Channel` -- Handle backpressure between the pipeline and the network - -## Key Files - -| File | Purpose | -|------|---------| -### Connection Management & Pooling - -| `src/TurboHTTP/Transport/Connection/ConnectionPool.cs` | Thread-safe per-host pool: `AcquireAsync`/`Release` API | -| `src/TurboHTTP/Transport/Connection/ConnectionLease.cs` | Single connection lease with busy/idle state, stream count, lifetime tracking | -| `src/TurboHTTP/Transport/Connection/ConnectionStage.cs` | Unified `GraphStage` bridging pipeline ↔ transport; delegates to `ITransportHandler` | -| `src/TurboHTTP/Pooling/ConnectionHandle.cs` | Bundles Channel read/write handles for direct TCP I/O | - -### Connection Scopes (Protocol-Aware Lifecycle) - -| `src/TurboHTTP/Transport/Connection/IConnectionScope.cs` | Interface: Acquire, Return, CanReuse, Cleanup, transport callback | -| `src/TurboHTTP/Transport/Connection/SingleRequestConnectionScope.cs` | HTTP/1.0: always new connection | -| `src/TurboHTTP/Transport/Connection/PersistentConnectionScope.cs` | HTTP/1.1+: reuse when keep-alive | -| `src/TurboHTTP/Transport/Connection/DeferredConnectionScope.cs` | Factory: defers scope creation until first request provides TcpOptions | - -### TCP/TLS Transport - -| `src/TurboHTTP/Transport/Tcp/ITransportHandler.cs` | Strategy interface: `TcpTransportHandler` (TCP) or `QuicTransportHandler` (QUIC) | -| `src/TurboHTTP/Transport/Tcp/TcpTransportHandler.cs` | TCP/TLS single-stream handler | -| `src/TurboHTTP/Transport/Tcp/ClientState.cs` | Per-connection state: inbound/outbound `Channel`, `Pipe`, stream direction | -| `src/TurboHTTP/Transport/Tcp/ClientByteMover.cs` | Async read/write pump between `Stream` and `Channel` | -| `src/TurboHTTP/Transport/Tcp/TcpOptionsFactory.cs` | Builds `TcpOptions`/`TlsOptions` from URI + client config | - -### QUIC/HTTP3 Transport - -| `src/TurboHTTP/Transport/Quic/QuicTransportHandler.cs` | QUIC multi-stream handler | -| `src/TurboHTTP/Transport/Quic/QuicConnectionManager.cs` | QUIC connection + stream lifecycle management | - -### Utilities - -| `src/TurboHTTP/Transport/DirectConnectionFactory.cs` | Establishes new TCP/TLS/QUIC connections | -| `src/TurboHTTP/Internal/Messages.cs` | Pipeline message types: `DataItem`, `ConnectItem`, `CloseSignalItem`, etc. | - -## Data Flow - -```text -Pipeline (IOutputItem) Network - │ │ - ▼ │ -┌─────────────────┐ │ -│ ConnectionStage │ ──── ITransportHandler ─────── │ -│ (GraphStage) │ ┌─────────────────────┐ │ -│ │ │ TcpTransportHandler │ │ -│ Dispatches: │ │ or │ │ -│ ConnectItem │ │ QuicTransportHandler │ │ -│ DataItem │ └────────┬────────────┘ │ -│ ControlItem │ │ │ -└────────┬────────┘ ▼ │ - │ ┌──────────────────┐ │ - │ │ ClientState │ │ - │ │ ┌──────────────┐ │ │ - │ │ │OutboundWriter│─┼──► Stream.WriteAsync() - │ │ └──────────────┘ │ │ - │ │ ┌──────────────┐ │ │ - ◄──────────────┼─│InboundReader │◄┼──── Stream.ReadAsync() - │ │ └──────────────┘ │ │ - (IInputItem) │ ┌──────────────┐ │ │ - │ │ Pipe │ │ (reassembly buffer) - │ └──────────────┘ │ │ - └──────────────────┘ -``` - -### Message Types - -Items flow between the pipeline and transport via marker interfaces: - -| Interface | Direction | Examples | -|-----------|-----------|---------| -| `IOutputItem` | Pipeline → Network | `DataItem`, `ConnectItem`, `ConnectionReuseItem` | -| `IInputItem` | Network → Pipeline | `DataItem`, `CloseSignalItem` | -| `IControlItem` | Pipeline → Network (non-data) | `ConnectItem`, `MaxConcurrentStreamsItem`, `StreamAcquireItem` | - -## Connection Pool Design - -### Version-Aware Strategy - -```text -┌──────────────────────────────────────────────────┐ -│ ConnectionPool │ -│ ConcurrentDictionary│ -│ │ -│ HTTP/1.0: Always new (no reuse) │ -│ HTTP/1.1: Idle queue, 6 per host (RFC 9112 §9.4)│ -│ HTTP/2+: MRU multiplexing, unlimited slots │ -└──────────────────────────────────────────────────┘ -``` - -| Version | Acquire Strategy | Release Strategy | Limit | -|---------|-----------------|------------------|-------| -| HTTP/1.0 | Always `EstablishAndTrack` | Always dispose | None | -| HTTP/1.1 | Try idle queue → wait semaphore → establish | Reusable → idle queue; else dispose + release semaphore | 6/host | -| HTTP/2+ | MRU with available stream slots → establish | Decrement streams; dispose when 0 streams + non-reusable | Unlimited | - -### Idle Eviction - -A `Timer` runs at `_idleTimeout` intervals calling `EvictIdle()`. Stale connections are disposed but **at least one connection per host is always kept** to avoid cold-start latency. - -### RequestEndpoint as Pool Key - -Connections are grouped by `(Scheme, Host, Port, Version)` — a `readonly record struct` with case-insensitive host/scheme comparison. This ensures HTTP/1.1 and HTTP/2 connections to the same host are pooled separately. - -## Channels-Based I/O (Actor-Free) - -### Why Not Actors? - -Traditional Akka.NET patterns use actors for connection management. TurboHTTP deliberately avoids this: - -1. **Lower overhead** — `Channel` has zero allocation for unbounded writes vs actor mailbox message wrapping -2. **Simpler debugging** — no actor hierarchy to trace; standard async/await stack traces -3. **Direct backpressure** — `Channel.Writer.WaitToWriteAsync` maps naturally to TCP flow control -4. **Compatibility** — `System.IO.Pipelines.Pipe` provides zero-copy buffer management aligned with .NET runtime optimizations - -### ClientState: The Connection Bundle - -Each connection is represented by a `ClientState` containing: -- **Inbound Channel** — network reads → pipeline consumption -- **Outbound Channel** — pipeline writes → network sends -- **Pipe** — `System.IO.Pipelines.Pipe` for reassembling partial reads into protocol frames -- **Stream Direction** — `Bidirectional`, `ReadOnly`, or `WriteOnly` (QUIC unidirectional streams) - -Buffer sizing scales with `MaxFrameSize`: -- ≤128KB → 512KB pause threshold -- ≤1MB → 2MB pause threshold -- >1MB → 2× max frame size - -### ClientByteMover: The Async Pump - -`ClientByteMover` runs two async loops per connection: -1. **Read pump**: `Stream.ReadAsync()` → `Pipe.Writer` → `InboundChannel.Writer` -2. **Write pump**: `OutboundChannel.Reader` → `Stream.WriteAsync()` - -On read completion, it sets `ClientState.CloseKind` to distinguish clean TLS `close_notify` from abrupt TCP RST — this signal propagates as `CloseSignalItem` so decoders know whether partial responses are valid (RFC 9112 §9.8). - -## ConnectionStage: The Bridge - -`ConnectionStage` is a `GraphStage>` that sits between the protocol engine and the network. It takes an `IConnectionScope` for connection lifecycle management: - -1. Receives the first `ConnectItem` and lazily creates an `ITransportHandler` (TCP or QUIC based on options type) -2. Routes `DataItem` writes to the outbound channel -3. **Auto-reconnect**: when `DataItem` arrives with `_handle == null` (HTTP/1.0, or HTTP/1.1 after Connection: close), acquires a new connection via `scope.AcquireAsync()` using stored options -4. Pumps inbound channel reads as `IInputItem` downstream -5. Handles max-concurrent-streams updates and stream acquire requests -6. Manages connect timeouts via `TimerGraphStageLogic` -7. **Transport callback**: `TcpTransportHandler` registers `OnTransportReturned` via `scope.RegisterTransportCallback()` — called by `ConnectionReuseFlowStage` after response evaluation - -### Handler Strategy Pattern - -```text -ConnectionStage delegates to: - ├── TcpTransportHandler (HTTP/1.x, HTTP/2) - │ └── Single bidirectional Stream - └── QuicTransportHandler (HTTP/3) - └── Multiple uni/bidirectional QUIC streams - ├── Control stream (SETTINGS, GOAWAY) - ├── QPACK encoder stream - └── Request streams (per-request) -``` - -## IConnectionScope: Protocol-Aware Connection Lifecycle - -`IConnectionScope` abstracts protocol-specific connection lifecycle (acquire, use, return) so the pipeline doesn't need protocol-aware branches: - -```text -┌─ Per-host substream (GroupByHostKey) ──────────────────────────┐ -│ │ -│ IConnectionScope (shared within fused substream actor) │ -│ AcquireAsync() ←── ConnectionStage (first data / reconnect)│ -│ ReturnAsync() ←── ConnectionReuseFlowStage (on response) │ -│ RegisterTransportCallback(Action) ──→ TcpTransportHandler │ -│ │ -│ SingleRequestConnectionScope (HTTP/1.0): │ -│ Always new connection, always close │ -│ PersistentConnectionScope (HTTP/1.1+): │ -│ Reuse if keep-alive, close on Connection: close │ -└────────────────────────────────────────────────────────────────┘ -``` - -(See Key Files section above for scope implementation locations in `src/TurboHTTP/Transport/Connection/`.) - -**Signal flow:** `ConnectionReuseFlowStage` calls `scope.ReturnAsync(canReuse)` → scope invokes registered callback → `TcpTransportHandler.OnTransportReturned(canReuse)` does cleanup (stop pump, clear handle, increment gen). All synchronous within the fused actor — no graph edges needed. - -## Known Limitations - -- **No connection prewarming** — connections are established on first request, not proactively -- **No DNS refresh** — `RequestEndpoint` caches the resolved host; DNS TTL changes require new connections -- **QUIC multi-stream complexity** — `QuicConnectionManager` handles stream multiplexing but the `Http3TaggedItem`/`Http3InputTaggedItem` routing adds indirection - -## Integration Points - -| Component | Interaction | -|-----------|-------------| -| [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] | `ConnectionStage` is wired into `ProtocolCoreGraphBuilder` per-version substreams | -| [[Architecture/Layers/13-CLIENT_LAYER|Client Layer]] | `ConnectionPool` is created by client factory, shared across named clients | -| [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]] | Encoders produce `DataItem` (outbound); decoders consume `DataItem` (inbound) | -| [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION|Diagnostics]] | `TurboHttpMetrics.ConnectionActive/Idle/Duration` track pool state | - -## See Also - -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — Layer positioning -- [[Architecture/Analysis/07-HTTP10_RECONNECTION_LIMITATION|HTTP/1.0 Reconnection Limitation]] — ExtractOptionsStage single-emit constraint -- [[Architecture/Analysis/11-STAGE_COMPLETION_AUDIT|Stage Completion Audit]] — ConnectionStage completion handling diff --git a/notes/Architecture/Layers/15-STREAMS_LAYER.md b/notes/Architecture/Layers/15-STREAMS_LAYER.md deleted file mode 100644 index 209919788..000000000 --- a/notes/Architecture/Layers/15-STREAMS_LAYER.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: Streams Layer -description: >- - Akka.Streams pipeline architecture — stage categories, BidiFlow stacking, - version demux, and data-flow diagrams -tags: - - architecture - - streams - - akka - - stages - - pipeline ---- -# Streams Layer - -The Streams Layer is TurboHTTP's core — it composes Akka.Streams `GraphStage` and `BidiFlow` components into a reactive pipeline that transforms `HttpRequestMessage` into `HttpResponseMessage`. Every HTTP feature (redirect, retry, caching, compression, cookies) is a composable BidiFlow stage. - -> **Scope**: This note covers pipeline composition and stage organization. For individual encoder/decoder internals, see [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]]. For stage patterns and naming, see [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]]. - -## Purpose - -- Compose HTTP features as stackable BidiFlow stages -- Route requests to version-specific protocol engines (HTTP/1.0, 1.1, 2, 3) -- Demultiplex per-host connections via `GroupByHostKey` / `MergeSubstreams` -- Provide the request/response correlation between outbound and inbound data - -## Key Files - -| File | Purpose | -|------|---------| -| `src/TurboHTTP/Streams/Engine.cs` | Top-level pipeline builder — stacks feature BidiFlows via `Atop` | -| `src/TurboHTTP/Streams/ProtocolCoreGraphBuilder.cs` | Version-demux graph: Partition → 4 protocol flows → Merge | -| `src/TurboHTTP/Streams/PipelineDescriptor.cs` | Aggregates optional policies for conditional BidiFlow insertion | -| `src/TurboHTTP/Streams/IProtocolEngine.cs` | Interface for per-version BidiFlow factories | -| `src/TurboHTTP/Streams/Http10Engine.cs` | HTTP/1.0 BidiFlow assembly | -| `src/TurboHTTP/Streams/Http11Engine.cs` | HTTP/1.1 BidiFlow assembly | -| `src/TurboHTTP/Streams/Http20Engine.cs` | HTTP/2 BidiFlow assembly | -| `src/TurboHTTP/Streams/Http30Engine.cs` | HTTP/3 BidiFlow assembly | - -## Full Pipeline Data Flow - -```text -HttpRequestMessage - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Feature BidiFlow Chain │ -│ (outermost → innermost, composed via Atop) │ -│ │ -│ ┌──────────────┐ │ -│ │ Tracing │ Creates root "TurboHTTP.Request" Activity│ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Handler[0] │ User middleware (outermost) │ -│ │ Handler[N] │ User middleware (innermost) │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Redirect │ RFC 9110 §15.4 — internal feedback loop │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Cookie │ RFC 6265 §5.3–§5.4 — jar inject/extract │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Retry │ RFC 9110 §9.2 — internal feedback loop │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Expect 100 │ RFC 9110 §10.1.1 — Expect: 100-continue │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Cache │ RFC 9111 — short-circuit on cache hit │ -│ └──────┬───────┘ │ -│ ┌──────┴───────┐ │ -│ │ Content │ RFC 9110 §8.4 — compress req / decomp res│ -│ │ Encoding │ │ -│ └──────┬───────┘ │ -└─────────┼───────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Protocol Engine Core (Island 2) │ -│ │ -│ ┌────────────────────────┐ │ -│ │ RequestEnricherStage │ Applies defaults, base address │ -│ └────────┬───────────────┘ │ -│ ▼ │ -│ ┌──── Partition (by Version) ────┐ │ -│ │ Out0: 1.0 Out1: 1.1 Out2: 2.0 Out3: 3.0 │ -│ └──┬────────┬────────┬────────┬──┘ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌──────┐┌──────┐┌──────┐┌──────┐ │ -│ │H10 ││H11 ││H20 ││H30 │ Per-version subflow │ -│ │Engine││Engine││Engine││Engine│ │ -│ └──┬───┘└──┬───┘└──┬───┘└──┬───┘ │ -│ └───┬────┘───┬────┘───┬────┘ │ -│ ▼ │ -│ ┌── Merge(4) ──┐ │ -│ └───────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -HttpResponseMessage -``` - -## Stage Categories - -### Encoding Stages (`Streams/Stages/Encoding/`) - -Transform `HttpRequestMessage` into wire-format `DataItem` bytes. - -| Stage | Protocol | Shape | Purpose | -|-------|----------|-------|---------| -| `Http10EncoderStage` | HTTP/1.0 | BidiFlow | Request → HTTP/1.0 text encoding | -| `Http11EncoderStage` | HTTP/1.1 | BidiFlow | Request → HTTP/1.1 text encoding | -| `Http20EncoderStage` | HTTP/2 | BidiFlow | Request → HPACK-compressed headers + DATA frames | -| `Http20PrependPrefaceStage` | HTTP/2 | Flow | Prepends connection preface (`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`) | -| `Http20Request2FrameStage` | HTTP/2 | Flow | Serializes `Http2Frame` structs to raw bytes | -| `Http30EncoderStage` | HTTP/3 | BidiFlow | Request → QPACK-compressed headers + DATA frames | -| `Http30ControlStreamPrefaceStage` | HTTP/3 | Flow | Prepends control stream type + SETTINGS frame | -| `Http30QpackEncoderPrefaceStage` | HTTP/3 | Flow | Prepends QPACK encoder stream type byte | -| `Http30Request2FrameStage` | HTTP/3 | Flow | Serializes `Http3Frame` structs to raw bytes | -| `QpackEncoderStreamStage` | HTTP/3 | Flow | Processes QPACK encoder instructions | - -### Decoding Stages (`Streams/Stages/Decoding/`) - -Transform inbound `DataItem` bytes into `HttpResponseMessage`. - -| Stage | Protocol | Shape | Purpose | -|-------|----------|-------|---------| -| `Http10DecoderStage` | HTTP/1.0 | BidiFlow | HTTP/1.0 text → response headers + body | -| `Http11DecoderStage` | HTTP/1.1 | BidiFlow | HTTP/1.1 text → response (chunked, content-length, close-delimited) | -| `Http20DecoderStage` | HTTP/2 | BidiFlow | Raw bytes → `Http2Frame` → response headers + DATA | -| `Http20StreamStage` | HTTP/2 | BidiFlow | Per-stream frame routing and reassembly | -| `Http20ConnectionStage` | HTTP/2 | Custom | Connection-level frame handling (SETTINGS, PING, GOAWAY, WINDOW_UPDATE) | -| `Http30DecoderStage` | HTTP/3 | BidiFlow | Raw bytes → `Http3Frame` → response headers + DATA | -| `Http30StreamStage` | HTTP/3 | BidiFlow | Per-stream frame routing | -| `Http30ConnectionStage` | HTTP/3 | Custom | Connection-level frame handling for QUIC | -| `QpackDecoderStreamStage` | HTTP/3 | Flow | Processes inbound QPACK decoder instructions | -| `QpackDecoderFeedbackStage` | HTTP/3 | Sink | Acknowledges QPACK table updates | - -### Feature Stages (`Streams/Stages/Features/`) - -Cross-cutting HTTP features implemented as BidiFlows. - -| Stage | RFC | Shape | Purpose | -|-------|-----|-------|---------| -| `TracingBidiStage` | — | BidiFlow | Root `Activity` lifecycle per request | -| `HandlerBidiStage` | — | BidiFlow | Wraps user `TurboHandler` middleware | -| `RedirectBidiStage` | RFC 9110 §15.4 | BidiFlow | Follows redirects with internal feedback loop | -| `CookieBidiStage` | RFC 6265 §5.3–§5.4 | BidiFlow | Injects/extracts cookies via `CookieJar` | -| `RetryBidiStage` | RFC 9110 §9.2 | BidiFlow | Retries idempotent requests with internal feedback loop | -| `ExpectContinueBidiStage` | RFC 9110 §10.1.1 | BidiFlow | Manages `Expect: 100-continue` handshake | -| `CacheBidiStage` | RFC 9111 | BidiFlow | Short-circuits on cache hit; stores responses | -| `ContentEncodingBidiStage` | RFC 9110 §8.4 | BidiFlow | Request compression + response decompression | -| `ConnectionReuseFlowStage` | RFC 9112 §9 | Flow | Evaluates keep-alive/close per response; calls `IConnectionScope.ReturnAsync()` | -| `DeadlockWatchdogStage` | — | Flow | DEBUG-only: detects pipeline stalls | - -### Routing Stages (`Streams/Stages/Routing/`) - -Control request/response routing within the pipeline. - -| Stage | Purpose | -|-------|---------| -| `RequestEnricherStage` | Applies `TurboRequestOptions` defaults to outgoing requests | -| `ExtractOptionsStage` | Splits first request into `ConnectItem` signal + request stream (simplified: no reconnect feedback) | -| `GroupByHostKeyStage` | Groups requests by `RequestEndpoint` into per-host substreams | -| `MergeSubstreamsStage` | Merges per-host response substreams back into a single stream | -| `HostKeyGroupByExtensions` | Extension methods for fluent `GroupByHostKey` syntax | -| `HostKeyMergeBack` | Merge-back helper for host-grouped substreams | -| `Http1XCorrelationStage` | Correlates HTTP/1.x request/response pairs (one-at-a-time) | -| `Http20CorrelationStage` | Correlates HTTP/2 requests/responses by stream ID | -| `Http20StreamIdAllocatorStage` | Assigns odd stream IDs to HTTP/2 client requests | -| `Http30CorrelationStage` | Correlates HTTP/3 requests/responses by QUIC stream | -| `Http30StreamDemuxStage` | Routes tagged output to correct QUIC stream type | - -## BidiFlow Stacking Pattern - -Feature stages are composed via `Atop` — each BidiFlow wraps the next, forming a bidirectional pipeline: - -```text -Request direction: Handler[0] → Handler[N] → Redirect → Cookie → Retry → Expect100 → Cache → ContentEncoding → Engine -Response direction: Engine → ContentEncoding → Cache → Expect100 → Retry → Cookie → Redirect → Handler[N] → Handler[0] -``` - -Only BidiFlows for non-null policies are included. The stacking is built from innermost to outermost in `Engine.BuildExtendedPipeline()`. - -## Per-Version Engine Assembly - -Each `IHttpProtocolEngine` implementation assembles a `BidiFlow`: - -```text -HTTP/1.0: Http10EncoderStage ↔ Http10DecoderStage + Http1XCorrelationStage -HTTP/1.1: Http11EncoderStage ↔ Http11DecoderStage + Http1XCorrelationStage -HTTP/2: Http20EncoderStage + PrependPreface + Request2Frame - ↔ Http20DecoderStage + ConnectionStage + StreamStage + CorrelationStage + StreamIdAllocator -HTTP/3: Http30EncoderStage + ControlStreamPreface + QpackEncoderPreface + Request2Frame - ↔ Http30DecoderStage + ConnectionStage + StreamStage + CorrelationStage + StreamDemux - + QpackDecoderStream + QpackDecoderFeedback + QpackEncoderStream -``` - -## Per-Host Substreaming - -`ProtocolCoreGraphBuilder` wraps each version's engine with `GroupByHostKey` → connection flow → `MergeSubstreams`. This materializes a **fresh pipeline copy per unique (host, port, scheme)** so connections are never mixed across hosts: - -```text -Partition(version) - │ - ▼ -GroupByHostKey(RequestEndpoint.FromRequest, maxSubstreams) - │ - ├── Substream host-A ──► Engine BidiFlow ◄──► ConnectionStage ──► TCP - ├── Substream host-B ──► Engine BidiFlow ◄──► ConnectionStage ──► TCP - └── ... - │ -MergeSubstreams - │ - ▼ -Merge(4 versions) -``` - -## Per-Host Connection Flow (Linear Topology) - -`BuildConnectionFlow()` in `ProtocolCoreGraphBuilder` assembles a linear pipeline per host substream using `IConnectionScope` for connection lifecycle: - -```text -Request → ExtractOptions → Engine.encode → MergePreferred → ConnectionStage → Engine.decode - (simplified) (ConnectItem (scope-managed) - priority) │ - ConnectionReuseFlow → Response - (scope.ReturnAsync) -``` - -**Key properties:** -- **Zero graph cycles** — no backward edges from response path to request path -- **Zero junction stages** except one `MergePreferred` for first `ConnectItem` priority -- **Scope-mediated reuse** — `ConnectionReuseFlowStage` calls `scope.ReturnAsync(canReuse)` which triggers a transport callback synchronously within the fused actor; no graph edges needed -- **Auto-reconnect** — when `DataItem` arrives with `_handle == null` (HTTP/1.0 every request, HTTP/1.1 after Connection: close), `TcpTransportHandler` re-acquires via `scope.AcquireAsync()` using stored options -- **Per-host scope** — `SingleRequestConnectionScope` (HTTP/1.0) or `PersistentConnectionScope` (HTTP/1.1+), created per substream by `GroupByHostKey` - -This replaced the previous feedback loop topology (Broadcast + 2× MergePreferred + ExtractOptionsStage.InReuse) which caused DL-006, DL-009, and DL-010 deadlocks. - -## Design Decisions - -### BidiFlow over DelegatingHandler - -Using Akka.Streams BidiFlows for features (redirect, retry, cache) instead of .NET's `DelegatingHandler` chain provides: -- **Backpressure-aware** — features naturally participate in stream flow control -- **Bidirectional** — a single stage intercepts both request and response paths -- **Composable** — `Atop` stacking is associative and order-independent for non-interacting features - -### Async Boundary at Engine Core - -`ProtocolCoreGraphBuilder.Build()` wraps the engine flow in `Attributes.CreateAsyncBoundary()`, ensuring the protocol engine runs on its own dispatcher. This prevents slow encode/decode work from blocking the feature BidiFlow chain. - -### DEBUG-Only DeadlockWatchdogStage - -In debug builds, `DeadlockWatchdogStage` is inserted at three pipeline points to detect backpressure stalls. It fires `TurboHttpDiagnosticListener.OnDeadlockStall` if no element flows within `WarningThreshold` (default 10s). Removed in release builds to avoid overhead. - -## Known Limitations - -- **No dynamic pipeline reconfiguration** — policies are fixed at pipeline materialization time -- **GroupByHostKey maxSubstreams** — HTTP/1.x allows 256, HTTP/2/3 allows 64; exceeding these requires substream eviction -- **Single async boundary** — all protocol versions share one boundary; under extreme load, version contention is possible - -## Integration Points - -| Component | Interaction | -|-----------|-------------| -| [[Architecture/Layers/13-CLIENT_LAYER|Client Layer]] | `Engine.CreateFlow()` is the main entry point | -| [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] | `ConnectionStage` wired inside each per-host substream | -| [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]] | Encoder/decoder stages use `Protocol/` classes for wire format | -| [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION|Diagnostics]] | `TracingBidiStage` + `DeadlockWatchdogStage` emit diagnostic events | - -## See Also - -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Port naming and stage lifecycle conventions -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]] — Three-layer decoder pattern -- [[Architecture/Analysis/11-STAGE_COMPLETION_AUDIT|Stage Completion Audit]] — Completion propagation bug fixes diff --git a/notes/Architecture/Layers/16-PROTOCOL_LAYER.md b/notes/Architecture/Layers/16-PROTOCOL_LAYER.md deleted file mode 100644 index 1a0c94b63..000000000 --- a/notes/Architecture/Layers/16-PROTOCOL_LAYER.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: Protocol Layer Architecture -description: >- - Encoder/decoder patterns, HPACK/QPACK internals, component folder structure, - and wire-format handling for HTTP/1.x, HTTP/2, and HTTP/3 -tags: - - architecture - - protocol - - encoders - - decoders - - hpack - - qpack ---- -# Protocol Layer Architecture - -## Purpose - -The Protocol layer (`src/TurboHTTP/Protocol/`) implements wire-format encoding and decoding for all supported HTTP versions. Each HTTP version and cross-cutting concern gets its own component subfolder containing encoders, decoders, and version-specific business logic. Shared codecs (HPACK under `Http2/Hpack/`, QPACK under `Http3/Qpack/`, Huffman at the root) are consumed by multiple protocol versions. - -This layer sits **below** the Streams layer (which orchestrates stage graphs) and **above** the Transport layer (which moves raw bytes). Protocol types convert between `HttpRequestMessage`/`HttpResponseMessage` and the `IOutputItem`/`IInputItem` message protocol used by the pipeline. - -> **Extends, does not repeat**: For how protocol flows are composed into the pipeline, see [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]]. For the three-layer decoder pattern, see [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]]. - ---- - -## Key Files - -| Component | Path | Role | -|-----------|------|------| -| HTTP/1.1 Encoder | `Protocol/Http11/Http11Encoder.cs` | Serialises requests to HTTP/1.1 wire format | -| HTTP/1.1 Decoder | `Protocol/Http11/Http11Decoder.cs` | Parses HTTP/1.1 responses from byte stream | -| HTTP/1.0 Encoder | `Protocol/Http10/Http10Encoder.cs` | HTTP/1.0 request serialisation (no chunked) | -| HTTP/1.0 Decoder | `Protocol/Http10/Http10Decoder.cs` | HTTP/1.0 response parsing (Content-Length only) | -| HTTP/2 Request Encoder | `Protocol/Http2/Http2RequestEncoder.cs` | Frames requests into HTTP/2 binary format | -| HTTP/2 Frame Decoder | `Protocol/Http2/Http2FrameDecoder.cs` | Parses HTTP/2 frames into response events | -| HTTP/3 Request Encoder | `Protocol/Http3/Http3RequestEncoder.cs` | QUIC-based HTTP/3 request encoding | -| HTTP/3 Response Decoder | `Protocol/Http3/Http3ResponseDecoder.cs` | HTTP/3 response parsing from QUIC streams | -| HTTP/3 Frame Encoder | `Protocol/Http3/Http3FrameEncoder.cs` | Low-level HTTP/3 frame serialisation | -| HTTP/3 Frame Decoder | `Protocol/Http3/Http3FrameDecoder.cs` | Low-level HTTP/3 frame parsing | -| QUIC Variable-Length Int | `Protocol/Http3/QuicVarInt.cs` | QUIC variable-length integer codec | -| HPACK Encoder | `Protocol/Http2/Hpack/HpackEncoder.cs` | HTTP/2 header compression (RFC 7541) | -| HPACK Decoder | `Protocol/Http2/Hpack/HpackDecoder.cs` | HTTP/2 header decompression | -| QPACK Encoder | `Protocol/Http3/Qpack/QpackEncoder.cs` | HTTP/3 header compression (RFC 9204) | -| QPACK Decoder | `Protocol/Http3/Qpack/QpackDecoder.cs` | HTTP/3 header decompression | -| Huffman Codec | `Protocol/HuffmanCodec.cs` | Shared Huffman encoding/decoding for HPACK/QPACK | -| Well-Known Headers | `Protocol/WellKnownHeaders.cs` | Shared header name constants across all versions | -| Decode Result | `Protocol/HttpDecodeResult.cs` | Discriminated union for decoder output states | -| Http Decoder Error | `Protocol/HttpDecoderException.cs` | Decoder exception carrying `HttpDecodeError` enum | - ---- - -## Data Flow - -```text -┌─────────────────────────────────────────────────────────┐ -│ Streams Layer │ -│ (GraphStages: EncoderStage / DecoderStage wrappers) │ -└────────────┬────────────────────────────┬───────────────┘ - │ HttpRequestMessage │ HttpResponseMessage - ▼ ▲ -┌────────────────────────┐ ┌─────────────────────────────┐ -│ Protocol Encoder │ │ Protocol Decoder │ -│ │ │ │ -│ 1. Serialise headers │ │ 1. Parse frame/line │ -│ (HPACK/QPACK/text) │ │ 2. Decompress headers │ -│ 2. Frame body │ │ (HPACK/QPACK/text) │ -│ 3. Emit IOutputItem │ │ 3. Assemble response │ -│ (DataItem bytes) │ │ 4. Emit HttpResponseMessage│ -└────────────┬───────────┘ └─────────────┬───────────────┘ - │ IOutputItem │ IInputItem - ▼ ▲ -┌─────────────────────────────────────────────────────────┐ -│ Transport Layer │ -│ (ConnectionStage → TCP/QUIC) │ -└─────────────────────────────────────────────────────────┘ -``` - -### Header Compression Flow (HTTP/2) - -```text -Request Headers ──► HpackEncoder ──► HEADERS frame bytes - │ - DynamicTable - (shared state) - │ -Response HEADERS ──► HpackDecoder ──► Decoded Headers -``` - -### Header Compression Flow (HTTP/3) - -```text -Request Headers ──► QpackEncoder ──► HEADERS + Encoder Stream - │ │ - DynamicTable QPACK instructions - │ │ - ▼ ▼ -Response HEADERS ◄── QpackDecoder ◄── Decoder Stream feedback -``` - ---- - -## Encoder/Decoder Pattern - -All protocol versions follow a consistent pattern: - -### Encoder Contract - -1. **Input**: `HttpRequestMessage` from the Streams layer -2. **Header serialisation**: Version-specific format (text lines for HTTP/1.x, HPACK-compressed HEADERS frames for HTTP/2, QPACK-compressed for HTTP/3) -3. **Body framing**: Identity/chunked (HTTP/1.x), DATA frames with flow control (HTTP/2), DATA frames on QUIC streams (HTTP/3) -4. **Output**: `IOutputItem` (typically `DataItem` wrapping `IMemoryOwner`) to Transport - -### Decoder Contract - -1. **Input**: `IInputItem` (raw bytes from Transport) -2. **Frame/line parsing**: Extract protocol units (HTTP/1.x lines, HTTP/2 frames, HTTP/3 frames) -3. **Header decompression**: Reverse of encoder header compression -4. **Response assembly**: Build `HttpResponseMessage` with headers and body stream -5. **Output**: `HttpResponseMessage` to Streams layer - -### Three-Layer Decoder Architecture - -HTTP/2 and HTTP/3 decoders use a three-layer pipeline (detailed in [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]]): - -```text -ConnectionStage (connection-level frames: SETTINGS, GOAWAY, PING) - └── StreamStage (per-stream demux and state machine) - └── DecoderStage (frame → HttpResponseMessage assembly) -``` - -### `HttpDecodeResult` Discriminated Union - -Decoders return `HttpDecodeResult` to signal parsing state: - -- **`NeedMoreData`** — Incomplete frame/message; request more bytes -- **`HeadersComplete`** — Headers fully parsed; body may follow -- **`Complete`** — Full response assembled -- **`Error`** — Protocol violation detected - ---- - -## HPACK Internals (RFC 7541) - -HPACK compresses HTTP/2 headers using a combination of: - -1. **Static Table** — 61 pre-defined header name/value pairs (e.g., `:method: GET`, `:status: 200`) -2. **Dynamic Table** — FIFO table of recently-seen headers, bounded by `SETTINGS_HEADER_TABLE_SIZE` -3. **Huffman Coding** — Optional per-octet Huffman encoding using the RFC 7541 code table - -### Encoding Decisions - -The encoder chooses per-header: -- **Indexed** (1 byte reference) — header exists in static or dynamic table -- **Literal with indexing** — header added to dynamic table for future reference -- **Literal without indexing** — transient headers (e.g., `:path`) not worth caching -- **Literal never indexed** — sensitive headers (e.g., `Authorization`) excluded from compression - -### Dynamic Table Eviction - -When a new entry exceeds `SETTINGS_HEADER_TABLE_SIZE`, oldest entries are evicted FIFO. The table size can be updated mid-connection via SETTINGS frames, triggering immediate eviction. - ---- - -## QPACK Internals (RFC 9204) - -QPACK adapts HPACK for HTTP/3's unordered QUIC streams: - -1. **Static Table** — Extended to 99 entries (superset of HPACK's 61) -2. **Dynamic Table** — Same concept but with **out-of-order insertion acknowledgment** -3. **Encoder Stream** — Unidirectional QUIC stream carrying table update instructions -4. **Decoder Stream** — Unidirectional QUIC stream carrying insertion acknowledgments - -### Key Difference from HPACK - -HPACK relies on TCP ordering — encoder and decoder see frames in the same order, so dynamic table state is always synchronised. QPACK cannot assume ordering, so it uses: - -- **Required Insert Count** — Each HEADERS block declares how many dynamic table inserts it depends on -- **Blocked streams** — Decoder may block a stream until the required inserts arrive on the encoder stream -- **Section Acknowledgment** — Decoder tells encoder which HEADERS blocks it has processed, allowing encoder to evict safely - ---- - -## Component Folder Structure - -```text -src/TurboHTTP/Protocol/ -├── HuffmanCodec.cs # Shared — HPACK + QPACK (RFC 7541 Appendix B) -├── WellKnownHeaders.cs # Shared header name constants across all versions -├── HttpDecodeResult.cs # Discriminated union: NeedMoreData/HeadersComplete/Complete/Error -├── HttpDecoderException.cs # Decoder exception carrying HttpDecodeError enum -├── HttpDecoderError.cs # Error code enum -├── Http10/ # HTTP/1.0 (RFC 1945) -│ ├── Http10Encoder.cs -│ └── Http10Decoder.cs -├── Http11/ # HTTP/1.1 (RFC 9112) -│ ├── Http11Encoder.cs -│ ├── Http11Decoder.cs -│ ├── ConnectionReuseDecision.cs -│ └── ConnectionReuseEvaluator.cs -├── Http2/ # HTTP/2 (RFC 9113) -│ ├── Http2RequestEncoder.cs -│ ├── Http2FrameDecoder.cs -│ ├── Http2Frame.cs -│ ├── Http2Exception.cs -│ └── Hpack/ # HPACK header compression (RFC 7541) -│ ├── HpackEncoder.cs -│ ├── HpackDecoder.cs -│ └── HpackException.cs -├── Http3/ # HTTP/3 (RFC 9114) -│ ├── Http3RequestEncoder.cs -│ ├── Http3ResponseDecoder.cs -│ ├── Http3FrameEncoder.cs -│ ├── Http3FrameDecoder.cs -│ ├── Http3Frame.cs -│ ├── Http3Settings.cs -│ ├── Http3Exception.cs -│ ├── Http3ErrorCode.cs -│ ├── Http3StreamType.cs -│ ├── Http3ControlStream.cs -│ ├── Http3UniStream.cs -│ ├── Http3RequestStream.cs -│ ├── QuicVarInt.cs # QUIC variable-length integer codec (RFC 9000 §16) -│ └── Qpack/ # QPACK header compression (RFC 9204) -│ ├── QpackEncoder.cs -│ ├── QpackDecoder.cs -│ ├── QpackDynamicTable.cs -│ ├── QpackStaticTable.cs -│ ├── QpackIntegerCodec.cs -│ ├── QpackStringCodec.cs -│ ├── QpackTableSync.cs -│ └── … -├── Semantics/ # HTTP Semantics (RFC 9110) -│ ├── RedirectPolicy.cs -│ ├── RetryPolicy.cs -│ ├── ContentEncodingEncoder.cs -│ ├── ContentEncodingDecoder.cs -│ └── … -├── Caching/ # HTTP Caching (RFC 9111) -│ ├── 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 -``` - -### Namespace Mapping - -| Component Folder | Namespace | RFC(s) | -|-----------------|-----------|--------| -| `Protocol/Http10/` | `TurboHTTP.Protocol.Http10` | RFC 1945 | -| `Protocol/Http11/` | `TurboHTTP.Protocol.Http11` | RFC 9112 | -| `Protocol/Http2/` | `TurboHTTP.Protocol.Http2` | RFC 9113 | -| `Protocol/Http2/Hpack/` | `TurboHTTP.Protocol.Http2.Hpack` | RFC 7541 | -| `Protocol/Http3/` | `TurboHTTP.Protocol.Http3` | RFC 9114 | -| `Protocol/Http3/Qpack/` | `TurboHTTP.Protocol.Http3.Qpack` | RFC 9204 | -| `Protocol/Semantics/` | `TurboHTTP.Protocol.Semantics` | RFC 9110 | -| `Protocol/Caching/` | `TurboHTTP.Protocol.Caching` | RFC 9111 | -| `Protocol/Cookies/` | `TurboHTTP.Protocol.Cookies` | RFC 6265 | - -### Naming Convention - -- Encoders: `Http{version}Encoder.cs` — one per wire-format version -- Decoders: `Http{version}Decoder.cs` — paired with encoder -- Component subfolder name matches the protocol version (e.g., `Http2/` for HTTP/2, `Semantics/` for cross-cutting concerns) - ---- - -## Design Decisions - -1. **Component-per-folder organisation** — Each HTTP version and cross-cutting concern gets its own component subfolder, making compliance tracking straightforward. Shared codecs (HPACK under `Http2/Hpack/`, QPACK under `Http3/Qpack/`, Huffman at the root) are nested under their primary consumer. - -2. **Stateless encoders, stateful decoders** — Encoders are largely stateless (HPACK/QPACK state is injected). Decoders maintain parsing state machines because responses can arrive incrementally across multiple `IInputItem` deliveries. - -3. **`IMemoryOwner` for zero-copy** — Encoded output uses pooled memory (`ArrayPool`) wrapped in `IMemoryOwner` to minimise allocations on the hot path. The Transport layer returns buffers to the pool after writing to the socket. - -4. **Shared Huffman codec** — `HuffmanCodec` is used by both HPACK and QPACK since they share the same Huffman table (RFC 7541 Appendix B). This avoids code duplication and ensures consistent encoding. - -5. **Separate QPACK streams** — HTTP/3 QPACK uses dedicated unidirectional QUIC streams for encoder/decoder communication. These are modelled as separate GraphStages (`QpackEncoderStreamStage`, `QpackDecoderStreamStage`) in the Streams layer, keeping protocol logic in the Protocol layer and stream orchestration in Streams. - ---- - -## Known Limitations - -- **No server push** — HTTP/2 server push (PUSH_PROMISE) is parsed but not acted upon; frames are discarded. This matches industry trend (Chrome disabled server push in 2022). -- **QPACK blocked streams limit** — Currently hardcoded; not configurable via `SETTINGS_QPACK_BLOCKED_STREAMS`. Sufficient for typical client usage but may need tuning for high-concurrency scenarios. -- **HTTP/1.0 no chunked transfer** — By RFC, HTTP/1.0 does not support chunked encoding. The encoder uses Content-Length only, which requires the full body to be buffered before sending. -- **Dynamic table size negotiation** — HPACK/QPACK dynamic table sizes respect server SETTINGS but the client does not proactively reduce table size to save memory on idle connections. - ---- - -## Integration Points - -| Boundary | Direction | Contract | -|----------|-----------|----------| -| Streams → Protocol | Inbound | `HttpRequestMessage` via `IHttpProtocolEngine.CreateFlow()` BidiFlow | -| Protocol → Transport | Outbound | `IOutputItem` (`DataItem`, `ConnectItem`) via BidiFlow outlet | -| Transport → Protocol | Inbound | `IInputItem` (`DataItem` with raw bytes) via BidiFlow inlet | -| Protocol → Streams | Outbound | `HttpResponseMessage` via BidiFlow outlet | -| HPACK ↔ HTTP/2 Encoder/Decoder | Internal | `HpackEncoder`/`HpackDecoder` injected into HTTP/2 codec | -| QPACK ↔ HTTP/3 Encoder/Decoder | Internal | `QpackEncoder`/`QpackDecoder` + dedicated stream stages | -| Huffman ↔ HPACK/QPACK | Internal | `HuffmanCodec` shared static utility | - ---- - -## See Also - -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE|Decoder Pipeline Architecture]] — Three-layer decoder pattern in detail -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — GraphStage wrappers that host protocol encoders/decoders -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Raw byte transport below the protocol layer -- [[Architecture/Design/02-STAGE_PATTERNS|GraphStage Patterns]] — Port naming and stage lifecycle conventions -- [[Architecture/Analysis/11-STAGE_COMPLETION_AUDIT|Stage Completion Audit]] — Completion propagation bugs found in protocol stages diff --git a/notes/Architecture/Layers/_INDEX.md b/notes/Architecture/Layers/_INDEX.md deleted file mode 100644 index c1b98af6a..000000000 --- a/notes/Architecture/Layers/_INDEX.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Layers Index -description: 'Index of per-layer architecture notes — client, transport, streams, protocol' -tags: - - architecture - - layers - - index ---- -# Layers - -Per-layer deep dives into each architectural layer of TurboHTTP. - -## Notes - -- [[Architecture/Layers/13-CLIENT_LAYER|Client Layer]] — Public API surface, factory pattern, DI integration, and request lifecycle -- [[Architecture/Layers/14-TRANSPORT_LAYER|Transport Layer]] — Actor-free connection pool, Channels-based I/O, TCP/TLS/QUIC transport, and backpressure model -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — Akka.Streams pipeline architecture — stage categories, BidiFlow stacking, version demux -- [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer Architecture]] — Encoder/decoder patterns, HPACK/QPACK internals, RFC subfolder structure, and wire-format handling diff --git a/notes/Architecture/Performance/01-BOTTLENECK_ANALYSIS_APR2026 b/notes/Architecture/Performance/01-BOTTLENECK_ANALYSIS_APR2026 deleted file mode 100644 index 19efc4695..000000000 --- a/notes/Architecture/Performance/01-BOTTLENECK_ANALYSIS_APR2026 +++ /dev/null @@ -1,112 +0,0 @@ ---- -tags: - - performance - - bottleneck - - H1.1 - - H2 - - allocation - - throughput -created: '2026-04-04' -updated: '2026-04-04' ---- -# TurboHTTP Performance Bottleneck Analysis — April 2026 - -**Benchmark Gap Summary:** -- H1.1 streaming 10k requests: 1.65x slower, 2x allocation -- H2 16-concurrent: 3x slower throughput -- H2 256-concurrent: 4x slower throughput -- H1.1 256-concurrent: 1.85x slower - ---- - -## Critical Findings - -### HIGH-IMPACT ALLOCATION HOTSPOTS - -#### 1. HTTP/1.1 Streaming Body Materialization (Http11DecoderStage) -- **Problem**: Connection-close-delimited bodies accumulated in `List` with one copy per chunk + final reassembly -- **Location**: Lines 66, 213-240 of Http11DecoderStage.cs -- **Impact**: Explains 2x allocation gap on streaming (52MB vs 24MB) -- **Fix**: Stream body directly via `StreamContent` wrapping lazy chunk iterator, avoid materialization - -#### 2. ClientByteMover Redundant Copy -- **Problem**: Redundant copy from `PipeReader` → rented buffer → channel → downstream. Data flows 4x through buffers. -- **Location**: Lines 71-77 of ClientByteMover.cs -- **Impact**: 10-15% H2 throughput loss -- **Fix**: Rent buffer directly from socket, pass to channel without intermediate copy - -#### 3. Http20StreamStage Per-Stream Allocations -- **Problem**: Each H2 stream allocates header buffer (16KB), body buffer, ContentHeaders list. Reallocations during fragmented headers. -- **Location**: Lines 39-122 of Http20StreamStage.cs -- **Impact**: 100+MB pressure on 256 concurrent streams -- **Fix**: Implement stream state pool, pre-allocate headers at RFC limit (16KB), reuse buffers - -#### 4. ContentEncodingBidiStage Full-Body Reads -- **Problem**: Double-buffering during compression/decompression (read → intermediate array → compress → new array) -- **Location**: Lines 228-268, 138-168 of ContentEncodingBidiStage.cs -- **Impact**: 5-10% allocation overhead -- **Fix**: Streaming compression/decompression, `StreamContent` wrappers - ---- - -### HIGH-IMPACT THROUGHPUT BOTTLENECKS - -#### 5. Excessive Async Callback Chain Depth (H2 Concurrent) -- **Problem**: H2 requests traverse 8+ stage boundaries with async callbacks (GetAsyncCallback). HttpClient has ~2. -- **Impact**: HIGH — Primary cause of 3-4x H2 concurrent slowdown -- **Fix Direction**: Merge stages (PrependPreface → Connection, RequestToFrame inline), implement direct synchronous dispatch on fast path - -#### 6. GroupByRequestEndpointStage Channel Overhead -- **Problem**: 256 concurrent requests = 256 channels being polled. Channel TryWrite + async callback per write. -- **Location**: Lines 410-412, 528-531 of GroupByRequestEndpointStage.cs -- **Impact**: 10-15% H2 throughput loss -- **Fix**: Lock-free slot selection, priority queue dispatch, avoid channel for fast path - -#### 7. ConnectionManagerActor Serialization (H2 Only) -- **Problem**: All connection acquisitions single-threaded through actor mailbox -- **Impact**: MEDIUM on H2 concurrent (256 pending connections queued) -- **Fix**: Shard actor, or lock-free concurrent queue for acquisition - -#### 8. Http20Engine Batch Consolidation Copies -- **Problem**: Frame batching consolidates multiple frames via memory copy -- **Location**: Lines 105-129 of Http20Engine.cs -- **Impact**: MEDIUM on H2 -- **Fix**: Smarter batching heuristic (avoid consolidation for large frames) - ---- - -### MEDIUM-IMPACT FINDINGS - -#### 9. Http20StreamStage Incremental Header Reallocation -- May reallocate header buffer multiple times as HEADERS + CONTINUATION frames arrive -- **Fix**: Fast-path for single-frame headers, pool reallocation overhead - -#### 10. Decoder Remainder Handling (Http11DecoderStage) -- Remainder data flushed and stored in body chunks list -- **Fix**: Keep remainder in decoder buffer, read on next pull - -#### 11. TcpConnectionStage ContinueWith Allocations -- Used for connection acquisition and outbound write callbacks -- **Fix**: Use `.UnsafeOnCompleted()` instead, inline FlushNext logic - -#### 12. ConnectionLease Volatile Reads -- Multiple volatile reads per request check -- **Fix**: Cache IsAlive state in local variable - ---- - -## Phase-1 Fix Priority - -1. **Http11DecoderStage: Streaming body (HIGH)** → 50% allocation reduction, 1.65x → 1.3x H1.1 speedup -2. **Async callback chain reduction (HIGH)** → Merge stages, 30-50% H2 concurrent improvement -3. **ClientByteMover copy elimination (MEDIUM)** → 10-15% H2 throughput -4. **Http20StreamStage buffer pooling (MEDIUM)** → Memory relief on H2 256-conc - ---- - -## Implementation Notes - -- All bottlenecks validated by code inspection; confirm with `dotnet-trace` or dotMemory profiling -- H2 multiplexing overhead is architectural — consider custom "Http2MultiplexStage" consolidating current 4-5 stages -- Benchmark on both light payloads (128B) and streaming (1MB+) to verify fix applicability -- Watch for regression on pipelined H1.1 scenarios when refactoring decoder diff --git a/notes/Architecture/Performance/PERFORMANCE_BOTTLENECK_ANALYSIS.md b/notes/Architecture/Performance/PERFORMANCE_BOTTLENECK_ANALYSIS.md deleted file mode 100644 index a65386d9f..000000000 --- a/notes/Architecture/Performance/PERFORMANCE_BOTTLENECK_ANALYSIS.md +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: TurboHTTP Performance Bottleneck Analysis -date: '2026-04-08' -type: analysis -status: actionable -tags: - - performance - - bottlenecks - - throughput - - allocations - - flow-control ---- -# TurboHTTP Performance Bottleneck Analysis - -> **Date:** 2026-04-08 -> **Scope:** Full pipeline deep-dive — Encoding, Decoding, Transport, Flow Control, Memory/Allocations -> **Method:** 5 parallel code analysis agents covering all hot paths - ---- - -## CRITICAL — Highest Impact - -### 1. HPACK/QPACK Dynamic Table: LinkedList O(n) Lookup - -Both dynamic tables use `LinkedList` with linear search per header reference. For 100 headers this means **~5,050 pointer dereferences** per response. - -| File | Lines | Issue | -|------|-------|-------| -| `Protocol/Http2/Hpack/HpackDecoder.cs` | 71-85 | `GetEntry()` — O(n) LinkedList walk per index | -| `Protocol/Http3/Qpack/QpackDynamicTable.cs` | 118-133 | `GetEntry()` — O(n) LinkedList walk per absolute index | -| `Protocol/Http3/Qpack/QpackEncoder.cs` | 509-550 | `FindDynamicExact()`/`FindDynamicName()` — linear search | - -**Fix:** Replace with `List` (index-based O(1)) or ring buffer with hash index. - ---- - -### 2. HTTP/2 Request Body: Triple-Copy Pattern - -A 10MB POST body gets **copied 3 times** before landing in frames: - -| File | Line | Copy | -|------|------|------| -| `Protocol/Http2/Http2RequestEncoder.cs` | 70 | HttpContent → MemoryStream | -| `Protocol/Http2/Http2RequestEncoder.cs` | 74 | MemoryStream → `new byte[bodyLen]` | -| `Protocol/Http2/Http2RequestEncoder.cs` | 93-100 | byte[] → 16KB frame chunks | - -**Impact:** ~7x memory overhead for large bodies. -**Fix:** Stream directly from HttpContent into frame chunks without intermediate buffers. - ---- - -### 3. HTTP/3 Encoding: Allocation per Header - -QPACK encoder allocates `Encoding.UTF8.GetBytes()` **per header field per request** (5-30 allocations/request): - -| File | Lines | -|------|-------| -| `Protocol/Http3/Qpack/QpackEncoder.cs` | 247, 254, 493, 502 | -| `Protocol/Http3/Qpack/QpackEncoderInstructionWriter.cs` | 77, 113-114 | - -**Fix:** `ArrayPool` with Span overload `GetBytes(string, Span)`. - ---- - -### 4. Graph Materialization per Substream - -`VersionDispatchStage` materializes the **entire engine pipeline** for every new endpoint group: - -| File | Lines | Issue | -|------|-------|-------| -| `Streams/Stages/Internal/VersionDispatchStage.cs` | 112-121 | `SubFusingMaterializer` creates all stage logics from scratch | - -**Impact:** 10 different endpoints = 10x full pipeline allocation (Encoder, Decoder, Correlation, Features). -**Fix:** Flow caching per (Version, Endpoint). - ---- - -### 5. HTTP/3 QUIC: Sequential Stream Opening - -`SemaphoreSlim(1)` serializes QUIC stream opening — destroys multiplexing benefit: - -| File | Lines | Issue | -|------|-------|-------| -| `Transport/Quic/QuicConnectionManager.cs` | 54-76 | `_spawnLock.WaitAsync()` blocks concurrent stream creation | - -**Fix:** Remove lock. - ---- - -## HIGH — Significant Impact - -### 6. HTTP/2 Flow Control: Receive Window Too Small - -Default `initialRecvWindowSize = 65535` bytes — at 50ms RTT this caps at **max ~1.3 Mbps per stream**. - -| File | Line | -|------|------| -| `Streams/Stages/Decoding/Http20ConnectionStage.cs` | 81 | - -**Fix:** Default to 1MB+, adapt based on BDP (Bandwidth-Delay Product). - ---- - -### 7. HTTP/2 Stream State Pool Too Small - -`StatePoolCapacity = 32`, but `maxConcurrentStreams = 100`. At CL>32 states are not recycled → GC churn: - -| File | Line | -|------|------| -| `Streams/Stages/Decoding/Http20ConnectionStage.cs` | 208 | - -**Fix:** Use direct "maxConcurrentStreams". - ---- - -### 8. HPACK/QPACK: Repeated UTF-8 GetByteCount Calls - -`EntrySize()` calls `Encoding.UTF8.GetByteCount()` **multiple times** for the same header (Add, Eviction, CheckSize): - -| File | Lines | -|------|-------| -| `Protocol/Http2/Hpack/HpackDecoder.cs` | 108, 215, 322 | -| `Protocol/Http3/Qpack/QpackDynamicTable.cs` | 164 | - -**Fix:** Cache byte-length at insertion time (store in header struct). - ---- - -### 9. HTTP/3 Frame Decoder: No Buffer Pooling - -Every fragmented frame allocates `new byte[]` without ArrayPool: - -| File | Lines | Issue | -|------|-------|-------| -| `Protocol/Http3/Http3FrameDecoder.cs` | 44, 62, 79 | `new byte[]` for combined/remainder | -| `Protocol/Http3/Http3FrameDecoder.cs` | 199, 204, 235 | `.ToArray()` for frame payloads | -| `Protocol/Http3/Http3ResponseDecoder.cs` | 123-149 | `List` body assembly with O(n²) copying | -| `Protocol/Http3/Qpack/QpackInstructionDecoder.cs` | 332 | `new byte[]` for combined buffer | - -**Fix:** `ArrayPool.Shared.Rent()` + `Memory` slices instead of `.ToArray()`. - ---- - -### 10. HTTP/1.0 Decoder: Excessive ToArray() - -Every response parse allocates multiple times via `.ToArray()`: - -| File | Lines | -|------|-------| -| `Protocol/Http10/Http10Decoder.cs` | 79, 111, 116, 141, 155, 165, 207, 247, 252 | -| `Protocol/Http10/Http10Decoder.cs` | 485 | `Combine()` — `new byte[]` without pooling | - ---- - -### 11. HuffmanCodec: MemoryStream + ToArray() - -Every encode/decode allocates MemoryStream and copies via `.ToArray()`: - -| File | Lines | -|------|-------| -| `Protocol/HuffmanCodec.cs` | 110-112 | `new MemoryStream()` + `.ToArray()` in Encode | -| `Protocol/HuffmanCodec.cs` | 138 | `new MemoryStream()` in Decode | - -**Fix:** Span-based with pre-sized buffer. - ---- - -## MEDIUM — Noticeable Under Load - -### 12. Batch Weight Too Conservative - -`MaxBatchWeight = 65536` (64KB) — at high throughput causes too many scheduler ticks: - -| File | Line | -|------|------| -| `Streams/Http20Engine.cs` | 16 | - -**Fix:** 256KB-512KB for high-throughput, adaptive. - ---- - -### 13. MemoryStream Allocations Scattered Everywhere - -~9+ locations create `new MemoryStream()` without pooling: - -| File | Context | -|------|---------| -| `Protocol/Http3/Http3RequestEncoder.cs:77` | Per-request body | -| `Protocol/Http10/Http10Encoder.cs:149` | Unknown-length body | -| `Protocol/Semantics/ContentEncodingEncoder.cs:52,63,74` | Compression | -| `Protocol/Semantics/ContentEncodingDecoder.cs:185` | Decompression | -| `Streams/Stages/Features/ContentEncodingBidiStage.cs:299-332` | Multiple instances | - -**Fix:** `RecyclableMemoryStreamManager`. - ---- - -### 14. Per-Request Collection Allocations - -`new List` / `new Dictionary` in hot paths: - -| File | Lines | What | -| -------------------------------------- | ------- | ------------------------------------------ | -| `Protocol/Http2/Http2FrameDecoder.cs` | 109 | `new List()` per decode | -| `Protocol/Http3/Http3FrameDecoder.cs` | 98 | `new List()` per decode | -| `Protocol/Http2/Hpack/HpackDecoder.cs` | 193 | `new List()` per header block | -| `Protocol/Http3/Qpack/QpackDecoder.cs` | 95, 140 | `new List<(string,string)>()` per decode | -| `Protocol/Cookies/CookieJar.cs` | 112 | `new List()` per request | - -**Fix:** `ArrayPool`-backed lists. - ---- - -### 15. TcpConnectionStage: Task.Run per Connection - -Every TCP connection spawns `Task.Run()` for the inbound pump: - -| File | Line | -|------|------| -| `Transport/Tcp/TcpConnectionStage.cs` | 523 | -| `Transport/Quic/QuicConnectionStage.cs` | 459 | - ---- - -### 16. QPACK Encoder Instruction Blocking - -When encoder instructions cannot be flushed, this **serializes all** subsequent requests: - -| File | Lines | -|------|-------| -| `Streams/Stages/Encoding/Http30Request2FrameStage.cs` | 92-96 | - ---- - -## LOW — Nice-to-Have - -| # | Issue | File:Line | -|---|-------|-----------| -| 17 | `QpackStringCodec` allocates Huffman-Encode just to check length | `Qpack/QpackStringCodec.cs:29` | -| 18 | `DateTime.UtcNow` per connection in eviction loop | `ConnectionManagerActor.cs:306` | -| 19 | `GroupByRequestEndpointStage.RemoveDead()` allocates `List` even when empty | `GroupByRequestEndpointStage.cs:159` | -| 20 | Socket buffer sizes not configurable | `IClientProvider.cs:100` | -| 21 | `HuffmanCodec._root` volatile instead of static initializer | `HuffmanCodec.cs:115` | -| 22 | NetworkBuffer pool unbounded (no cap) | `Messages.cs:80` | - ---- - -## Top 5 Quick Wins (Effort vs Impact) - -| # | Fix | Expected Impact | Effort | -|---|-----|-----------------|--------| -| 1 | HPACK/QPACK `LinkedList` → `List` | **~30% faster header decode** | 2-3h | -| 2 | HTTP/2 body: direct streaming instead of triple-copy | **~7x less memory for POST** | 4-6h | -| 3 | QPACK Encoder: `stackalloc`/`ArrayPool` instead of `GetBytes()` | **~20-30 fewer allocs/request** | 2-3h | -| 4 | HTTP/3 FrameDecoder: `ArrayPool` instead of `new byte[]` | **GC pressure significantly reduced** | 1-2h | -| 5 | Receive window → 1MB+ | **Throughput x10+ at latency >10ms** | 30min | - ---- - -## Next Steps - -- [ ] Create feature plans for top 5 quick wins -- [ ] Run BenchmarkDotNet baselines before changes -- [ ] Implement fixes in priority order -- [ ] Re-benchmark after each fix to measure actual impact diff --git a/notes/Architecture/Performance/TOP_5_THROUGHPUT_OPTIMIZATIONS.md b/notes/Architecture/Performance/TOP_5_THROUGHPUT_OPTIMIZATIONS.md deleted file mode 100644 index 9e7faf570..000000000 --- a/notes/Architecture/Performance/TOP_5_THROUGHPUT_OPTIMIZATIONS.md +++ /dev/null @@ -1,564 +0,0 @@ -# TOP 5 HIGH-IMPACT THROUGHPUT OPTIMIZATIONS - -**Analysis Date:** 2026-04-04 -**Focus:** HTTP/1.1 low-concurrency bottlenecks (CL=1-4) causing 40% throughput loss vs HttpClient -**Methodology:** Code inspection + benchmark analysis (188-222μs per request at CL=1) - ---- - -## OPTIMIZATION 1: Http2RequestEncoder Frame List Pooling -**Impact: +12-15μs per request | ~7-8% throughput gain** - -### Current Problem -**File:** `src/TurboHTTP/Protocol/RFC9113/Http2RequestEncoder.cs:49` - -```csharp -public (int StreamId, IReadOnlyList Frames) Encode(HttpRequestMessage request, int streamId) -{ - // ... header encoding ... - var frames = new List(); // NEW allocation per request - EncodeHeaders(frames, streamId, headerBlock, hasBody); - // ... body encoding ... - return (streamId, frames); -} -``` - -**Why It's Slow:** -- **Per-request allocation:** A new `List` (56 bytes) is allocated for every HTTP/2 request -- **At 250-260 ns each** (10% of per-request encoding time) -- **GC pressure:** Contributes to Gen0 collections visible in benchmarks (Gen0 allocations increase at CL=16+) -- **18 stages × ~2-3μs context switches** compounds this; saving allocations on hot path is critical - -**Estimated Slowness:** 10-15 microseconds per request (allocation + initialization + potential Gen0 collection pressure) - -### Concrete Fix -Create a reusable frame list pool using `ArrayPool` pattern: - -```csharp -// At class level -private readonly Stack> _frameListPool = new(capacity: 4); -private readonly object _poolLock = new(); - -// Rent -private List RentFrameList() -{ - lock (_poolLock) - { - return _frameListPool.Count > 0 ? _frameListPool.Pop() : new(capacity: 8); - } -} - -// Return after encoding -private void ReturnFrameList(List list) -{ - list.Clear(); - lock (_poolLock) - { - if (_frameListPool.Count < 4) - { - _frameListPool.Push(list); - } - } -} -``` - -Alternative (lock-free): Use `System.Collections.Concurrent.ConcurrentStack` for the pool, but be aware this moves allocation from List to ConcurrentStack overhead (minimal gain). - -**Better approach:** Since `Http2RequestEncoder` is per-connection and not shared, use a single reusable field: - -```csharp -private List _reusableFrames = new(); - -public (int StreamId, IReadOnlyList Frames) Encode(HttpRequestMessage request, int streamId) -{ - _reusableFrames.Clear(); - EncodeHeaders(_reusableFrames, streamId, headerBlock, hasBody); - // ... body encoding ... - return (streamId, _reusableFrames); -} -``` - -**Trade-off:** Caller must consume the list immediately (no async buffering). This is acceptable since frames are written to the transport layer synchronously. - -**Estimated Savings:** 10-15μs per request (allocation + field initialization) - ---- - -## OPTIMIZATION 2: CancellationTokenSource Linked-Token Pool -**Impact: +8-12μs per request | ~5-7% throughput gain** - -### Current Problem -**File:** `src/TurboHTTP/TurboHttpClient.cs:225-227` - -```csharp -CancellationTokenSource cts = cancellationToken.CanBeCanceled - ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) - : new CancellationTokenSource(); -using (cts) -{ - cts.CancelAfter(Timeout); - // ... await ... -} -``` - -**Why It's Slow:** -- **Per-request allocation:** `CancellationTokenSource.CreateLinkedTokenSource()` allocates: - - CTS instance (56 bytes) - - Internal registration list (capacity overhead) - - Registers with parent CTS (callback registration overhead) -- **Lock contention:** Parent `CancellationToken.CanBeCanceled` registration internally locks on the parent's registration list -- **At ~50-100ns per CTS creation**, this adds up significantly at high concurrency -- **Worse at low concurrency:** Timer registration via `CancelAfter()` involves `TimerQueue` which scales better at higher concurrency but degrades at CL=1-4 - -**Benchmark Evidence:** -- CL=1 HTTP/1.1 light: 188.9μs mean -- CL=4 HTTP/1.1 light: 197.8μs mean -- Pure linked CTS overhead is ~5-10% of total latency - -### Concrete Fix -Cache the CTS per TurboHttpClient instance and reset it between requests: - -```csharp -internal sealed class TurboHttpClient : ITurboHttpClient -{ - // Pool of reusable CTS instances - private static readonly Stack _ctsPools = new(); - private CancellationTokenSource? _reusableCts; - - public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var pending = PendingRequest.Rent(); - - try - { - await Manager.Requests.WriteAsync(request, cancellationToken); - - // Reuse or rent a CTS - var cts = _reusableCts ?? CreateOrRentCts(); - _reusableCts = null; - - using (cts) - { - cts.CancelAfter(Timeout); - using (cts.Token.UnsafeRegister( - static (state, ct) => ((PendingRequest)state!).TrySetCanceled(ct), - pending)) - { - return await pending.GetValueTask(); - } - } - } - finally - { - // Cache CTS for next request - _reusableCts = new CancellationTokenSource(); - // ... rest of cleanup ... - } - } - - private static CancellationTokenSource CreateOrRentCts() - { - lock (_ctsPools) - { - return _ctsPools.Count > 0 ? _ctsPools.Pop() : new(); - } - } - - private static void ReturnCts(CancellationTokenSource cts) - { - cts.Cancel(); // Reset state - cts.Dispose(); // Will be re-created - lock (_ctsPools) - { - if (_ctsPools.Count < 32) // Limit pool size - { - _ctsPools.Push(cts); - } - } - } -} -``` - -**Even Better:** Skip CTS for simple timeout case (no external CT): - -```csharp -public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) -{ - var pending = PendingRequest.Rent(); - - if (!cancellationToken.CanBeCanceled) - { - // No external cancellation — use a single reusable CTS for timeout only - using var cts = new CancellationTokenSource(Timeout); - using (cts.Token.UnsafeRegister( - static (state, ct) => ((PendingRequest)state!).TrySetCanceled(ct), - pending)) - { - return await pending.GetValueTask(); - } - } - else - { - // Linked token source required - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(Timeout); - // ... rest ... - } -} -``` - -**Estimated Savings:** 8-12μs per request (CTS allocation + registration overhead) - ---- - -## OPTIMIZATION 3: HeaderBlock Allocation in Http2RequestEncoder -**Impact: +6-8μs per request | ~3-4% throughput gain** - -### Current Problem -**File:** `src/TurboHTTP/Protocol/RFC9113/Http2RequestEncoder.cs:44-46` - -```csharp -_headerBlockWriter.Clear(); -_hpack.Encode(headers, _headerBlockWriter, _useHuffman); -var headerBlock = _headerBlockWriter.WrittenMemory.ToArray(); // NEW allocation -``` - -**Why It's Slow:** -- **Line 46 allocates a new byte array** for every request by calling `.ToArray()` -- `ArrayBufferWriter` (256-byte initial capacity) is reused ✓, but the header block itself is copied -- This allocation is **immediately passed to `EncodeHeaders()` which may re-slice it** (lines 150-151, 158) -- At ~500-800 byte headers, this is non-trivial allocation pressure - -**Benchmark Signal:** "7.58 KB" allocated at CL=1 light-payload is mostly these header allocations - -### Concrete Fix -Avoid `.ToArray()` and work directly with `WrittenMemory`: - -```csharp -public (int StreamId, IReadOnlyList Frames) Encode(HttpRequestMessage request, int streamId) -{ - var headers = BuildHeaderList(request); - ValidatePseudoHeaders(headers); - - _headerBlockWriter.Clear(); - _hpack.Encode(headers, _headerBlockWriter, _useHuffman); - - // Use WrittenMemory directly without .ToArray() - var headerBlockMemory = _headerBlockWriter.WrittenMemory; - var hasBody = request.Content != null; - - var frames = new List(); - EncodeHeadersFromMemory(frames, streamId, headerBlockMemory, hasBody); - // ... rest ... -} - -private void EncodeHeadersFromMemory(List frames, int streamId, ReadOnlyMemory headerBlock, bool hasBody) -{ - if (headerBlock.Length <= _maxFrameSize) - { - frames.Add(new HeadersFrame(streamId, headerBlock, endStream: !hasBody, endHeaders: true)); - return; - } - - // Fragmented case — work with Memory slices - frames.Add(new HeadersFrame(streamId, headerBlock[.._maxFrameSize], endStream: false, endHeaders: false)); - - var pos = _maxFrameSize; - while (pos < headerBlock.Length) - { - var chunkSize = Math.Min(headerBlock.Length - pos, _maxFrameSize); - var isLast = pos + chunkSize >= headerBlock.Length; - frames.Add(new ContinuationFrame(streamId, headerBlock[pos..(pos + chunkSize)], endHeaders: isLast)); - pos += chunkSize; - } -} -``` - -**Trade-off:** Ensure `HeadersFrame` and `ContinuationFrame` ctors accept `ReadOnlyMemory` (check if they currently require a byte[]). - -**Estimated Savings:** 6-8μs per request (header allocation overhead) - ---- - -## OPTIMIZATION 4: PendingRequest Lock Contention -**Impact: +5-10μs per request | ~3-6% throughput gain (scales with concurrency)** - -### Current Problem -**File:** `src/TurboHTTP/TurboHttpClient.cs:213-216, 241-244` - -```csharp -lock (_pendingLock) -{ - _pendingTcs.Add(pending); // Add to HashSet -} - -// ... await ... - -lock (_pendingLock) -{ - _pendingTcs.Remove(pending); // Remove from HashSet -} -``` - -**Why It's Slow:** -- **Lock contention at high concurrency:** All requests compete for `_pendingLock` -- At CL=1-4 (low concurrency), lock overhead is small (~50-100ns per lock) -- At CL=16+, this becomes significant (visible in benchmark: CL=16 H/1.1 light = 391.7μs vs CL=4 = 197.8μs) -- **HashSet allocation pressure:** Every `Add()` checks capacity; HashSet is sized for ~4-16 items by default - -**Benchmark Evidence:** -- CL=1: 188.9μs (minimal contention) -- CL=4: 197.8μs (still small lock cost) -- CL=16: 391.7μs (lock cost ~100-200μs across all requests) -- CL=64: 1913.1μs (severe contention) - -### Concrete Fix -Use lock-free tracking with `Interlocked` operations OR move tracking to a per-request token: - -**Option A: Interlocked counter (simplest)** -```csharp -private volatile int _pendingRequestCount; - -public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) -{ - var pending = PendingRequest.Rent(); - Interlocked.Increment(ref _pendingRequestCount); // No lock - - try - { - // ... send and await ... - } - finally - { - Interlocked.Decrement(ref _pendingRequestCount); - PendingRequest.Return(pending); - } -} - -public void CancelPendingRequests() -{ - // This method requires knowing which requests are pending, so we need the HashSet. - // But if CancelPendingRequests is rarely called, accept the lock here. -} -``` - -**Option B: Remove CancelPendingRequests tracking (if rarely used)** -```csharp -// Remove _pendingLock and _pendingTcs entirely if CancelPendingRequests is rarely called -// and clients can rely on Dispose() to cancel the underlying stream. -``` - -**Option C: Use ConcurrentBag (lock-free but allocating)** -```csharp -private readonly ConcurrentBag _pendingBag = new(); - -lock (_pendingLock) -{ - _pendingTcs.Add(pending); -} -// becomes: -_pendingBag.Add(pending); -``` - -The lock is only **necessary for `CancelPendingRequests()`**, which is likely a rare operation. Move the lock there: - -```csharp -private volatile HashSet _pendingSnapshot; - -public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) -{ - var pending = PendingRequest.Rent(); - // No lock needed here - - try - { - // ... - } - finally - { - PendingRequest.Return(pending); - } -} - -public void CancelPendingRequests() -{ - // Only lock here, and only if needed - lock (_pendingLock) - { - foreach (var pending in _pendingTcs) - { - pending.TrySetCanceled(); - } - _pendingTcs.Clear(); - } -} -``` - -**Estimated Savings:** 5-10μs per request (at high concurrency; minimal at CL=1-4) - ---- - -## OPTIMIZATION 5: ChannelSourceStage OnConsumed Callback Allocation -**Impact: +4-6μs per request | ~2-3% throughput gain** - -### Current Problem -**File:** `src/TurboHTTP/Streams/Stages/Internal/GroupByRequestEndpointStage.cs:413` - -```csharp -var channelStage = new ChannelSourceStage( - capacity: _queueSize, - onConsumed: () => consumedCallback((capturedKey, capturedState!))); -``` - -**Why It's Slow:** -- **Closure allocation per substream creation:** The lambda captures `capturedKey` and `capturedState` -- At pipeline startup (new connection), this creates a closure for each parallel slot -- The closure is invoked **on every consumed item** (every request) -- Closure allocation = ~40-50 bytes per substream slot -- At 18 Akka stages, each with their own context switches, eliminating this callback overhead saves CPU time - -**Secondary issue:** -**File:** `src/TurboHTTP/Streams/Stages/Internal/ChannelSourceStage.cs:104-112` - -```csharp -_onItemCallback = GetAsyncCallback(item => -{ - _waiting = false; - if (IsAvailable(_stage._out)) - { - Push(_stage._out, item); - _stage._onConsumed?.Invoke(); // Invokes closure per item - } -}); -``` - -The `?.Invoke()` on line 110 and 128 happens for **every request** flowing through the stage. - -### Concrete Fix -Avoid capturing state in closures; use a struct-based callback mechanism instead: - -```csharp -// In GroupByRequestEndpointStage -internal sealed class ConsumedCallbackHandler -{ - public RequestEndpoint Key { get; set; } - public SubflowState State { get; set; } - - public void Invoke() - { - // Handle the callback directly - } -} - -// Pass a reference instead of a closure -var handler = new ConsumedCallbackHandler { Key = key, State = state }; -var channelStage = new ChannelSourceStage( - capacity: _queueSize, - onConsumed: handler.Invoke); // Method group — no closure allocation -``` - -**Better yet:** Remove the callback entirely and use a **Task-based event** on `ChannelSourceStage`: - -```csharp -internal sealed class ChannelSourceStage : GraphStage> -{ - // Instead of Action, fire a Task that the GroupBy stage awaits - private readonly Channel<(RequestEndpoint Key, SubflowState State)> _onConsumedChannel = - Channel.CreateUnbounded<(RequestEndpoint, SubflowState)>(); - - public ChannelWriter<(RequestEndpoint Key, SubflowState State)> OnConsumedWriter => _onConsumedChannel.Writer; -} - -// In GroupByRequestEndpointStage -_onChannelConsumed = GetAsyncCallback<(RequestEndpoint Key, SubflowState State)>(tuple => -{ - // ... handle consumption ... -}); - -// Then in ChannelSourceStage, instead of onConsumed?.Invoke(), -// write to the channel: -await _onConsumedChannel.Writer.WriteAsync((key, state)); -``` - -**Trade-off:** Introduces an async task per consumption. If throughput is critical and allocation is the bottleneck, keep the method-group approach: - -```csharp -internal sealed class ChannelSourceStage : GraphStage> -{ - private readonly Action? _onConsumed; - - // Call it directly without ?.Invoke() overhead - internal void SignalConsumed() - { - if (_onConsumed != null) - { - _onConsumed(); // No null-check overhead from ?. - } - } -} -``` - -**Estimated Savings:** 4-6μs per request (callback invocation + closure allocation amortized) - ---- - -## SUMMARY TABLE - -| Optimization | File | Lines | Current Cost | Fix Type | Est. Savings | Cumulative | -|---|---|---|---|---|---|---| -| 1. Frame list pooling | Http2RequestEncoder.cs | 49 | 10-15μs | Pool reuse | 12-15μs | 12-15μs | -| 2. CTS linked-token pool | TurboHttpClient.cs | 225-227 | 8-12μs | Per-client cache | 8-12μs | 20-27μs | -| 3. HeaderBlock ToArray | Http2RequestEncoder.cs | 46 | 6-8μs | Memory-based | 6-8μs | 26-35μs | -| 4. Lock contention | TurboHttpClient.cs | 213-244 | 5-10μs | Lock-free | 5-10μs | 31-45μs | -| 5. Callback allocation | GroupByRequestEndpointStage.cs | 413 | 4-6μs | Method group | 4-6μs | 35-51μs | - -**Expected Total Improvement:** 35-51 microseconds per request at CL=1-4 -**At 188-222μs baseline:** ~16-23% throughput improvement - ---- - -## IMPLEMENTATION PRIORITY - -1. **FIRST:** Optimization #1 (Frame list pooling) - - Simplest fix, high impact, zero behavioral change - - One field + Clear() call - -2. **SECOND:** Optimization #2 (CTS pool) - - Medium complexity, proven pattern (PendingRequest already does this) - - Per-client singleton, reuse across requests - -3. **THIRD:** Optimization #3 (HeaderBlock) - - Requires frame constructors to accept Memory - - Audit HeadersFrame and ContinuationFrame ctors first - -4. **FOURTH:** Optimization #4 (Lock contention) - - Scaling benefit; only critical at CL=16+ - - Requires refactoring CancelPendingRequests logic - -5. **FIFTH:** Optimization #5 (Callback allocation) - - Smallest impact, affects only substream creation - - Relevant when frequent connection/slot rebalancing - ---- - -## VALIDATION APPROACH - -Run micro-benchmarks before/after each fix: - -```bash -# HTTP/1.1 low concurrency (target workload) -dotnet run --project TurboHTTP.Benchmarks -- \ - --filter "*ConcurrentRequests*" \ - --column Median --column StdDev \ - --job Dry - -# Measure GC impact -dotnet run --project TurboHTTP.Benchmarks -- \ - --filter "*ConcurrentRequests*" \ - --column Gen0 --column Gen1 --column "Allocated" -``` - -Expected regression test results: -- **Before:** CL=1 H/1.1 light: 188.9μs -- **After all fixes:** ~160-170μs (10-15% improvement) -- **GC benefit:** Reduced Gen0 allocations at CL=4+ due to list/CTS reuse diff --git a/notes/Architecture/Performance/_INDEX.md b/notes/Architecture/Performance/_INDEX.md deleted file mode 100644 index 6d67102fa..000000000 --- a/notes/Architecture/Performance/_INDEX.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Performance Index -description: >- - Index of performance analysis notes — bottleneck investigations and - optimization strategies -tags: - - architecture - - performance - - index ---- -# Performance - -Performance analysis, bottleneck investigations, and optimization recommendations for TurboHTTP. - -## Notes - -- [[Architecture/Performance/01-BOTTLENECK_ANALYSIS_APR2026|Bottleneck Analysis (Apr 2026)]] — Systematic bottleneck analysis with profiling data and prioritized recommendations -- [[Architecture/Performance/PERFORMANCE_BOTTLENECK_ANALYSIS|Performance Bottleneck Analysis]] — Deep-dive analysis of pipeline performance constraints -- [[Architecture/Performance/TOP_5_THROUGHPUT_OPTIMIZATIONS|Top 5 Throughput Optimizations]] — Highest-impact throughput improvements ranked by expected gain diff --git a/notes/Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS.md b/notes/Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS.md deleted file mode 100644 index 2b9fd5308..000000000 --- a/notes/Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS.md +++ /dev/null @@ -1,468 +0,0 @@ ---- -title: Known Gaps & Limitations -description: Critical issues, high-priority gaps, and recommended fixes before v1.0 production release -tags: [gaps, limitations, issues, roadmap, critical] -aliases: [KnownGaps, Limitations, Blockers, Issues] ---- - -# TurboHTTP Known Gaps & Limitations - -**Last Updated**: 2026-03-26 -**Severity Levels**: 🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low - -## Critical Gaps (Blocks Production) - -### 🔴 1. Server-Side Implementation Missing - -**Problem**: Only client-side HTTP client library exists. No server. - -**Impact**: Cannot build HTTP server applications with TurboHTTP. No symmetric API. - -**Current State**: -- Encoders (serialize HttpRequestMessage) ✅ exist -- Decoders (parse HttpResponseMessage) ✅ exist -- Server request parsing ❌ missing -- Server response encoding ❌ missing - -**Solution**: Post-v1.0 roadmap item. Requires: -1. New `/TurboHTTP/Server/` layer with `ITurboHttpServer` -2. Reverse of client pipeline: requests in, responses out -3. ASP.NET Core integration (MapTurboHttpServer middleware) - -**Timeline**: Estimated 8-12 weeks after v1.0 - ---- - -### 🔴 2. HTTP/3 QPACK Encoder Missing - -**Problem**: QPACK decoder exists (RFC 9204), encoder missing. Can't send HTTP/3 requests. - -**Impact**: HTTP/3 is write-only (can't write headers to wire format). - -**Current State**: -``` -RFC 9204 QPACK Implementation: -✅ Decoder (read compressed headers from wire) -❌ Encoder (write headers to wire format) -``` - -**Missing Code**: -- `QpackEncoder` class (mirrors `HpackEncoder` from RFC 7541) -- `QpackEncoderInstructionStream` for dynamic table updates -- `QpackFieldWriter` for field encoding -- Instruction processing (INSERT_WITH_NAME_REF, INSERT_LITERAL, DUPLICATE) - -**Solution**: -1. Study RFC 9204 §4.1 (encoder algorithm) -2. Implement `QpackEncoder` with synchronized table updates -3. Test against RFC 9204 §C (test vectors) -4. Integrate into `Http30EncoderStage` - -**Timeline**: 3-4 weeks estimated - ---- - -### 🔴 3. QUIC Transport Incomplete - -**Problem**: Only variable-length integers (RFC 9000 §16) implemented. Missing packet structure, handshake, ACK/loss detection. - -**Impact**: No actual QUIC over UDP. Only HTTP/3 frame parsing (which still requires QUIC below). - -**Current State**: -``` -RFC 9000 QUIC Implementation: -✅ Variable-length integers (QuicVarInt) -❌ Long form packet headers -❌ Packet number encoding -❌ Handshake (Initial/Handshake/Retry packets) -❌ Loss detection + congestion control -❌ Key update (1-RTT) -``` - -**Missing Code**: -- `QuicPacket` / `QuicPacketHeader` types -- `QuicHandshakeManager` (client TLS handshake integration) -- `QuicLossDetector` / `QuicCongestionController` -- `QuicStream` state machine -- `QuicConnection` manager (Connection ID, migration) - -**Why It's Hard**: -- QUIC handshake requires TLS 1.3 integration (Tls13 context) -- Loss detection is stateful and complex (rto, pto, etc.) -- Interop testing requires real servers (Google QUIC, cloudflare, etc.) - -**Solution**: -1. Integrate System.Net.Quic (.NET's native QUIC) as transport -2. OR implement full QUIC from scratch (10+ weeks) - -**Recommended**: Use `System.Net.Quic` — already ships with .NET 7+ - -**Timeline**: 4-6 weeks if using System.Net.Quic, 10+ weeks if from scratch - ---- - -## High-Priority Gaps (Feature Completeness) - -### 🟠 1. Header Size/Count DoS Protection - -**Problem**: No limits on header size or count. Large responses can OOM client. - -**Risk**: Malicious servers can crash client with: -```http -HTTP/1.1 200 OK\r\n -X-Large: [10MB header value]\r\n -X-Count: [10,000 headers]\r\n -``` - -**Current State**: -- No `MaxHeaderSize` limit -- No `MaxHeaderCount` limit -- No per-header-field size limit - -**RFC Guidance**: -- RFC 9110 §5 suggests reasonable limits -- RFC 9113 §6.5.2 recommends 16KB for HTTP/2 header blocks - -**Solution**: -```csharp -public class HttpDecoderLimits -{ - public int MaxHeaderSize = 16 * 1024; // 16KB total - public int MaxHeaderCount = 100; // 100 headers max - public int MaxSingleHeaderSize = 8 * 1024; // 8KB per header -} -``` - -**Implementation**: -- Add `HttpDecoderLimits` to decoder constructors -- Throw `HttpDecoderException` if exceeded -- Document sensible defaults - -**Timeline**: 2-3 hours - ---- - -### 🟠 2. HTTP/2 MAX_CONCURRENT_STREAMS Client Enforcement - -**Problem**: Server sends `SETTINGS_MAX_CONCURRENT_STREAMS`, client ignores it. Can't match server concurrency limits. - -**Current State**: -```csharp -// Client receives SETTINGS frame with MAX_CONCURRENT_STREAMS=100 -// But then tries to open stream 101, 102, … — no limit enforced! -``` - -**Impact**: Violates RFC 9113 §5.1.2. Causes server to RST_STREAM when limit exceeded. - -**Solution**: -1. Track `MaxConcurrentStreams` from server SETTINGS -2. Maintain `_activeStreamCount` counter -3. Block new stream allocation if limit reached -4. Emit `GOAWAY` if received RST_STREAM with FLOW_CONTROL_ERROR - -**Implementation**: -- Extend `Http20StreamIdAllocatorStage` to check limit -- Add backpressure mechanism (queue pending streams) -- Test with Kestrel H2 configured with low limits - -**Timeline**: 4-6 hours - ---- - -### 🟠 3. Redirect Loop Detection - -**Problem**: Infinite redirect chains (A→B→A→B) crash client with stack overflow or hang indefinitely. - -**Current State**: -```csharp -// No tracking of visited URLs -// No max-redirects limit (defaults to HTTP spec) -``` - -**RFC Guidance**: -- RFC 9110 §15.4 doesn't mandate limits but implies reasonable ones -- HTTP spec typically suggests 5-10 max redirects - -**Solution**: -```csharp -public class RedirectPolicy -{ - public int MaxRedirects = 10; // Configurable limit - public TimeSpan RedirectTimeout = TimeSpan.FromSeconds(30); -} - -// Track visited URLs in RedirectBidiStage -private readonly HashSet _visitedUrls = new(); -if (_visitedUrls.Contains(nextUri)) - throw new RedirectException($"Redirect loop detected: {nextUri}"); -``` - -**Implementation**: -- Add `RedirectPolicy` to `TurboClientOptions` -- Extend `RedirectBidiStage` to track visited URLs -- Throw `RedirectException` with loop details - -**Timeline**: 3-4 hours - ---- - -### 🟠 4. HTTPS→HTTP Downgrade Protection - -**Problem**: Server sends redirect from HTTPS→HTTP. Client follows without warning (security issue). - -**RFC Guidance**: -- RFC 9110 §15.4.6 recommends blocking cross-scheme downgrades for security - -**Current State**: -```csharp -// No checking of scheme changes -client.SendAsync(new() { RequestUri = new("https://example.com/") }) - // Server redirects to http://example.com/ - // Client follows silently — DATA EXPOSED! -``` - -**Solution**: -```csharp -if (originalRequest.RequestUri.Scheme == "https" && - redirectUri.Scheme == "http") -{ - throw new RedirectException("Cannot redirect from HTTPS to HTTP"); -} -``` - -**Implementation**: -- Add check in `RedirectBidiStage` -- Make it configurable: `AllowInsecureRedirects = false` (default: true for compatibility) - -**Timeline**: 1-2 hours - ---- - -## Medium-Priority Gaps (RFC Edges) - -### 🟡 1. Connection Pooling Per-Host Limits Not Enforced - -**Problem**: No documented limit on connections per host. Load tests can exhaust port ranges. - -**Current State**: -```csharp -var pool = new ConnectionPool(); -// Creates unlimited new connections to example.com -for (int i = 0; i < 10000; i++) - await pool.AcquireAsync(new("example.com", 80), opts); -``` - -**Windows Ephemeral Port Exhaustion**: -- Windows has ~16,384 ephemeral ports (49152–65535) -- TIME_WAIT lasts 120 seconds -- Creating 20,000 connections → exhausts ports → EADDRINUSE errors - -**Solution**: -```csharp -public class ConnectionPoolOptions -{ - public int MaxConnectionsPerHost = 10; // HTTP spec default - public int MaxTotalConnections = 100; // Global limit - public TimeSpan IdleConnectionTimeout = TimeSpan.FromSeconds(60); -} -``` - -**Implementation**: -- Document `HostConnections._limiter: SemaphoreSlim` semantics -- Add configurable limits to `ConnectionPool` -- Test with BenchmarkDotNet to validate - -**Timeline**: 4-6 hours - ---- - -### 🟡 2. Trailer Headers Not Supported (HTTP/1.1) - -**Problem**: RFC 9112 §6.1 defines trailer headers (headers after body chunks), but decoder ignores them. - -**Severity**: 🟢 Low — rarely used in practice (mostly for signing, checksums) - -**Current State**: -```http -POST / HTTP/1.1 -Transfer-Encoding: chunked - -5\r\n -Hello\r\n -0\r\n -X-Checksum: abc123\r\n ← Trailer (not parsed) -\r\n -``` - -**Solution**: -1. Extend `Http11DecoderPipeline` to parse trailer lines after `0\r\n` -2. Add `TrailerHeaders` to `HttpResponseMessage` (or `HttpContent.TrailingHeaders`) -3. Test with RFC compliance vectors - -**Timeline**: 6-8 hours - ---- - -### 🟡 3. Chunk Extensions Not Parsed (HTTP/1.1) - -**Problem**: RFC 9112 §6.1 allows extensions after chunk size, but decoder skips them. - -**Severity**: 🟢 Low — rarely used (reserved for future extensions) - -**Current State**: -```http -HTTP/1.1 200 OK -Transfer-Encoding: chunked - -5;ext=val\r\n ← Extension `;ext=val` ignored -Hello\r\n -0\r\n -\r\n -``` - -**RFC Example**: `5e3;name=value\r\n` (chunk size in hex with name-value pair) - -**Solution**: -1. Extend `Http11DecoderPipeline` to parse and validate extensions -2. Store in `ChunkExtensions` (or log and discard) -3. Test with RFC test vectors - -**Timeline**: 4-6 hours - ---- - -### 🟡 4. Public Suffix Cookies Not Enforced (RFC 6265) - -**Problem**: Cookies for bare domains (e.g., `example.com` vs `sub.example.com`) not validated against public suffix list. - -**Severity**: 🟢 Low — affects multi-tenant domains (e.g., github.io pages) - -**Current State**: -```csharp -var jar = new CookieJar(); -// Server: Set-Cookie: id=123; Domain=.github.io -// → Creates cookie for ALL github.io subdomains! -``` - -**RFC Guidance**: RFC 6265 §5.3 recommends consulting public suffix list - -**Solution**: -1. Embed Mozilla public suffix list (or load from https://publicsuffix.org/list/) -2. Check domain against list before setting cookies -3. Reject cookies for bare public domains - -**Timeline**: 4-6 hours (mostly data management) - ---- - -### 🟡 5. Server Push (HTTP/2) Minimally Implemented - -**Problem**: Clients receive PUSH_PROMISE frames but don't handle promised streams correctly. - -**Severity**: 🟡 Medium — server push rarely used (only ~2-3% of production HTTP/2) - -**Current State**: -```csharp -// Server: PUSH_PROMISE for /styles.css -// Client: Receives frame but doesn't validate promised stream -``` - -**RFC Requirement**: RFC 9113 §6.6 requires validating push promise constraints - -**Solution**: -1. Extend `Http20ConnectionStage` to validate PUSH_PROMISE -2. Create promised stream in reserved state -3. Allow server to send DATA on promised stream -4. Let client reject with RST_STREAM if not interested - -**Timeline**: 8-10 hours - ---- - -## Low-Priority Gaps (Advanced Features) - -### 🟢 1. QUIC Connection Migration (RFC 9000 §9) - -**Severity**: 🟢 Low — needed for mobile clients, not typical desktop/server use - -**Problem**: No support for changing IP/port mid-connection (happens on mobile network switch) - -**Solution**: Post-v1.0, requires `System.Net.Quic` integration - -**Timeline**: 2-3 weeks - ---- - -### 🟢 2. Alternative Service (Alt-Svc) Header - -**Severity**: 🟢 Low — rarely used (mostly CDNs) - -**Problem**: Ignore Alt-Svc header that advertises HTTP/3 upgrade - -**Solution**: Parse header, track alternative endpoints, test on next request - -**Timeline**: 3-4 hours - ---- - -### 🟢 3. Proxy Support (Proxy-Authorization, CONNECT) - -**Severity**: 🟢 Low — enterprise-only, not in v1.0 roadmap - -**Problem**: No support for HTTP proxy tunneling (CONNECT method) - -**Solution**: Post-v1.0 roadmap item - -**Timeline**: 4-5 weeks - ---- - -## Mitigations (Workarounds) - -| Gap | Workaround | -|-----|-----------| -| Server implementation missing | Use Kestrel for now; switch after v1.0 | -| HTTP/3 encoder missing | Stick with HTTP/1.1/2 for now; wait for HTTP/3 release | -| DoS protection | Implement own limits in `HttpMessageHandler` wrapping | -| Redirect loops | Wrap client with retry policy that tracks URLs | -| QUIC transport | Use `System.Net.Quic` as underlying transport (if available) | -| Trailer headers | Configure servers not to send trailers (most don't) | -| Chunk extensions | Ignore (not used in practice) | -| Public suffix cookies | Use own cookie policy layer above `CookieJar` | -| Server push | Disable with SETTINGS_ENABLE_PUSH = 0 | - ---- - -## Testing Gaps - -| Component | Unit Tests | Integration Tests | Compliance Tests | -|-----------|-----------|-------------------|-----------------| -| HTTP/1.0 | ✅ 233 | ✅ 15 | ✅ Complete | -| HTTP/1.1 | ✅ 374 | ✅ 45 | ✅ Complete | -| HTTP/2 | ✅ 545 | ✅ 60 | ✅ 85% | -| HTTP/3 | 🟡 < 50 | ❌ 0 | ❌ 0% | -| HPACK | ✅ 419 | ✅ 10 | ✅ 100% | -| QPACK | 🟡 < 50 | ❌ 0 | ❌ 0% | -| Caching | ✅ 75 | ✅ 20 | ✅ 80% | -| Cookies | ✅ 66 | ✅ 15 | ✅ 85% | - ---- - -## Recommended Fixes Before v1.0 - -**Priority 1** (MUST): -- [ ] DoS protection (header size/count limits) — 2-3 hours -- [ ] QPACK encoder — 3-4 weeks -- [ ] Expand RFC9110 tests — 1 week - -**Priority 2** (SHOULD): -- [ ] Redirect loop detection — 3-4 hours -- [ ] HTTPS→HTTP protection — 1-2 hours -- [ ] MAX_CONCURRENT_STREAMS enforcement — 4-6 hours - -**Priority 3** (NICE-TO-HAVE): -- [ ] Trailer headers support — 6-8 hours -- [ ] Chunk extensions parsing — 4-6 hours -- [ ] Public suffix cookies — 4-6 hours - -**Total Estimated Time**: 4-6 weeks for Priority 1+2, additional 1-2 weeks for Priority 3 diff --git a/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md b/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md deleted file mode 100644 index 4924287d1..000000000 --- a/notes/Architecture/Status/04-CURRENT_STATE_SUMMARY.md +++ /dev/null @@ -1,354 +0,0 @@ ---- -title: TurboHTTP Current State Summary -description: >- - Comprehensive snapshot of TurboHTTP implementation status, completion scores - by RFC, what works well, what needs work, and next milestones -tags: - - status - - implementation - - completeness - - milestones -aliases: - - Current State - - Project Status - - v1.0 Roadmap ---- -# TurboHTTP Current State Summary - -**Last Updated**: 2026-04-07 -**Version**: Pre-1.0 (Development) -**Branch**: `feature/better-graph` (main is `main`) - -## Project Status - -### Implementation Completeness: 75/100 - -``` -┌─────────────────────────────────────────────┐ -│ HTTP/1.0 ████████████░ 85/100 │ -│ HTTP/1.1 ████████████░ 92/100 │ -│ HTTP/2 ███████████░░ 87/100 │ -│ HTTP/3 ██████░░░░░░ 60/100 │ -│ HPACK ████████████░ 90/100 │ -│ QPACK ██░░░░░░░░░░ 40/100 │ -│ Cookies ████████░░░░ 80/100 │ -│ Caching ███████░░░░░ 78/100 │ -│ Redirects/Retries ████████░░░░ 82/100 │ -├─────────────────────────────────────────────┤ -│ Overall ██████████░░ 75/100 │ -└─────────────────────────────────────────────┘ -``` - -### Build & Test Status ✅ - -- **Build**: ✅ Compiles cleanly (Release mode) -- **Test Count**: 260 unit tests + 515 integration tests = **775 tests** -- **Test Pass Rate**: ✅ 100% (all passing) -- **Architecture**: ✅ Stable (layered, no breaking changes expected) -- **Dependencies**: ✅ Stable (.NET 10.0, Akka.Streams 1.5.63, xUnit v3) - -### What Works Well ✅ - -#### Client-Side HTTP Protocols -- ✅ HTTP/1.0 requests/responses (simple, 1 req per connection) -- ✅ HTTP/1.1 requests/responses (pipelining, keep-alive, chunked) -- ✅ HTTP/2 requests/responses (binary, multiplexing, flow control) -- ✅ HPACK header compression (fully RFC 7541 compliant) - -#### Core Features -- ✅ Cookie jar (RFC 6265) — domain/path/secure/HttpOnly/SameSite -- ✅ Cache store (RFC 9111) — freshness, validation, Vary support -- ✅ Redirect following (RFC 9110 §15.4) — 301/302/303/307/308 -- ✅ Idempotent retry (RFC 9110 §9.2) — Retry-After, exponential backoff -- ✅ Connection pooling — per-host keep-alive, async lease model -- ✅ Content decompression — gzip, deflate, brotli - -#### Architecture -- ✅ **Strict layered design** — Client → Handlers → Streams → Protocol → Transport -- ✅ **Actor-free data path** — Zero actor mailbox hops (uses Channels) -- ✅ **GraphStage-based** — Akka.Streams for multiplexing, backpressure -- ✅ **Memory efficient** — `Span`, `Memory`, zero-copy patterns -- ✅ **RFC-aligned** — Each layer maps to RFC requirements -- ✅ **DI-friendly** — Microsoft.Extensions integration, TurboHttpClientFactory - -#### Testing -- ✅ **Unit tests** organized by component (260 tests) -- ✅ **Integration tests** with Kestrel (515 tests) -- ✅ **Stream tests** with Akka.TestKit (GraphStage behavior) -- ✅ **Benchmark suite** (25+ benchmarks) - -### What Needs Work 🔶 - -#### HTTP/3 & QUIC -- 🔶 HTTP/3 protocol partially done (frame parsing, stream types) -- ❌ QPACK encoder missing (decoder exists) -- ❌ QUIC transport missing (only variable-length integers) -- 🔶 No integration tests (requires UDP + TLS) - -#### DoS Protection -- ❌ No header size limits (RFC 9110 §5) -- ❌ No header count limits (RFC 9110 §5) -- ❌ No request rate limiting - -#### Advanced Features -- 🔶 Redirect loop detection (not enforced) -- 🔶 HTTPS→HTTP downgrade (allowed, should block) -- 🔶 Trailer headers (HTTP/1.1 RFC 9112 §6.1 not parsed) -- 🔶 Chunk extensions (HTTP/1.1 RFC 9112 §6.1 not parsed) -- 🔶 Server push (HTTP/2 PUSH_PROMISE minimal support) - -#### Documentation & Release -- 🔶 No server-side implementation (TurboServer missing) -- 🔶 No production DI/logging integration -- 🔶 VitePress docs partially written -- 🔶 No NuGet package yet -- 🔶 No RELEASE_NOTES.md versioning - ---- - -## Architecture Highlights - -### 1. Layered Data Flow - -``` -User Code - ↓ -TurboHandler (delegating handler) - ↓ -Akka.Streams Graph - ├─ Engine (HTTP version demux) - │ ├─ Encoding (serialize request) - │ ├─ Decoding (parse response) - │ ├─ Features (redirect, retry, cache, cookies) - │ └─ Routing (multiplexing, correlation) - ├─ Protocol Layer (encoders, decoders, business logic) - └─ Transport (connection pool, channels, TCP/QUIC) - ↓ -TCP/QUIC -``` - -Each layer is independent: -- Layers only depend on layers **below** them -- Protocol layer is RFC authority -- Streams layer orchestrates features -- Client layer provides DI-friendly API - -### 2. Actor-Free Data Path - -``` -No actor mailbox in: TCP → Channels → Akka.Streams → Response - -Why? -- Zero GC pressure from actor message queues -- Direct backpressure from downstream (no actor indirection) -- Faster request/response round-trip -``` - -### 3. GraphStage Conventions - -- **Port Names**: `StageName.In` / `StageName.Out` (PascalCase) -- **No port prefix**: Already in class name (HttpEncoder not Http.Encoder) -- **Semantic roles**: `Request`/`Response`/`Final`/`Redirect`/etc. -- **Globally unique**: No two stages share names - -Example: -```csharp -"Http11Encoder.In" → "Http11Encoder.Out" // FlowShape -"Redirect.In" → "Redirect.Out.Final" / "Redirect.Out.Redirect" // FanOut -``` - -### 4. Protocol Layer Organization - -``` -Protocol/ -├── HuffmanCodec.cs (shared — HPACK + QPACK) -├── WellKnownHeaders.cs (shared header name constants) -├── Http10/ (HTTP/1.0 — RFC 1945) -├── Http11/ (HTTP/1.1 — RFC 9112) -├── Http2/ (HTTP/2 — RFC 9113) -│ └── Hpack/ (HPACK header compression — RFC 7541) -├── Http3/ (HTTP/3 — RFC 9114) -│ └── Qpack/ (QPACK header compression — RFC 9204) -├── Semantics/ (HTTP Semantics — RFC 9110) -├── Caching/ (HTTP Caching — RFC 9111) -└── Cookies/ (Cookie management — RFC 6265) -``` - -### 5. Connection Pool Design - -``` -ConnectionPool -└── HostConnections (per host:port) - ├── _idle: Queue (keep-alive connections) - ├── _limiter: SemaphoreSlim(N) (per-host concurrency limit) - ├── _evictionTimer (idle timeout) - └── SelectMru() (select most-recently-used) - -ConnectionLease -├── ConnectionHandle (channel wrappers) -├── ClientState (TCP stream, pipes) -└── Lifecycle (MarkBusy, MarkIdle, MarkNoReuse) -``` - -**Key**: No actors, purely async/await with Channels - ---- - -## Key Invariants & Constraints - -### Memory Management -- ✅ `ReadOnlyMemory` for buffer efficiency -- ✅ `Span` for zero-copy ref parameters -- ✅ `IMemoryOwner` for buffer lifetime -- ✅ `ArrayPool` for temporary buffers - -### Error Handling -- `HpackException` → RFC 7541 violations -- `Http2Exception` → HTTP/2 protocol errors -- `HttpDecoderException` → decode failures + `HttpDecodeError` enum -- `RedirectException` → redirect logic errors - -### CancellationToken -- ✅ Flows through all async call chains -- ✅ No `.Result` or `.Wait()` (always async) -- ✅ No `async void` (always `Task`/`Task`) -- ✅ Timeout via `CancellationTokenSource` or `[Fact(Timeout=ms)]` - -### Thread Safety -- ✅ `ConnectionPool` is thread-safe (SemaphoreSlim, ConcurrentQueue) -- ✅ `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 -- ✅ All tests have explicit timeouts (no hanging tests) -- ✅ Max 500 lines per test file (split if needed) -- ✅ `[Trait("RFC", "RFC-
")]` for RFC traceability (post-Feature-040) -- ✅ Use `[Theory]` + `[InlineData]` for parameterized tests - ---- - -## Recent Changes (2026-04) - -### Features 047–052: Protocol Namespace Reorganisation ✅ -- Protocol layer reorganised into component-based subfolders (Http10, Http11, Http2, Http3, Semantics, Caching, Cookies) -- All namespaces updated: `TurboHTTP.Protocol.` -- Obsidian vault updated to reflect component folder structure - -### Features 040–046: Test Organisation + Transport Split ✅ -- Test files migrated from RFC-numbered folders to component-based folders -- Transport layer split into Connection/, Tcp/, Quic/ subfolders - ---- - -## Next Major Milestones - -### Before v1.0 (Estimated 6-8 weeks) -1. **Stability** (1-2 weeks) - - [ ] Header DoS protection (size/count limits) - - [ ] Redirect loop detection - - [ ] HTTPS→HTTP protection - -2. **HTTP/3** (3-4 weeks) - - [ ] QPACK encoder implementation - - [ ] HTTP/3 stream lifecycle completion - - [ ] Integration tests with Kestrel H3 - -3. **Testing** (1 week) - - [ ] Expand RFC9110 tests - - [ ] Benchmark-driven validation - -4. **Release** (1 week) - - [ ] NuGet packaging - - [ ] RELEASE_NOTES.md - - [ ] Documentation site - -### Post-v1.0 Roadmap -1. **TurboServer** (server-side implementation) -2. **OpenTelemetry** (metrics, tracing, logging) -3. **Advanced Features** (public suffix, datagram, migration) -4. **Performance Tuning** (SIMD, streaming, GC optimization) - ---- - -## Resource Locations - -| Resource | Path | -|----------|------| -| **Source Code** | `src/TurboHTTP/` | -| **Unit Tests** | `src/TurboHTTP.Tests/` (organized by component) | -| **Stream Tests** | `src/TurboHTTP.StreamTests/` (Akka.Streams behavior) | -| **Integration Tests** | `src/TurboHTTP.IntegrationTests/` (Kestrel fixtures) | -| **Benchmarks** | `src/TurboHTTP.Benchmarks/` (BenchmarkDotNet) | -| **Documentation** | `docs/` (VitePress) | -| **Obsidian Vault** | `notes/` (architecture, RFC notes, decisions) | -| **Feature Plans** | Internal planning directory (feature_NNN.md) | -| **Diagnostics** | `.ralph/runs/` (automation logs) | - ---- - -## Build & Development - -### Build Commands -```bash -# Build all -dotnet build --configuration Release ./src/TurboHTTP.sln - -# Run all tests -dotnet test ./src/TurboHTTP.sln - -# Run tests for a component -dotnet test ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- \ - --filter-namespace "TurboHTTP.Tests.Http2" - -# Run tests with specific RFC trait -dotnet test ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- \ - --filter "Trait~RFC9113" - -# Run benchmarks -dotnet run --configuration Release ./src/TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj -``` - -### Development Workflow -1. Create feature branch from `main` -2. Implement in `src/TurboHTTP/` and tests in `src/TurboHTTP.Tests/` -3. Add `[Trait("RFC", "RFC-
")]` for RFC traceability (e.g., `[Trait("RFC", "RFC9113-4.1")]`) -4. Ensure max 500 lines per test file -5. Run full test suite (`dotnet test`) -6. Create PR to `main` for review - -### Documentation -- Architecture decisions → `notes/Architecture/` (ADR template) -- RFC compliance notes → `notes/RFC/` (RFC-Note template) -- Feature plans → internal planning directory (feature_NNN.md) -- Session work → `notes/Sessions/` (Session-Log template) - ---- - -## Quality Gates - -Before committing code: -- ✅ `dotnet build --configuration Release` succeeds -- ✅ `dotnet test ./src/TurboHTTP.sln` passes (100%) -- ✅ No new compiler warnings (TreatWarningsAsErrors enabled) -- ✅ Test files ≤ 500 lines -- ✅ All async tests have explicit timeouts -- ✅ `[Trait("RFC", ...)]` attributes on tests for RFC traceability - -Before creating PR: -- ✅ All quality gates passing -- ✅ RFC compliance verified (spec-aligned) -- ✅ Memory safe (`Span`, `Memory` patterns) -- ✅ Thread-safe (no race conditions) -- ✅ Documented in CLAUDE.md (conventions used) - ---- - -## Key Contacts & References - -- **RFC Editor**: https://www.rfc-editor.org/ -- **HTTP/2 Spec** (RFC 9113): https://www.rfc-editor.org/rfc/rfc9113 -- **HTTP/3 Spec** (RFC 9114): https://www.rfc-editor.org/rfc/rfc9114 -- **QUIC Spec** (RFC 9000): https://www.rfc-editor.org/rfc/rfc9000 -- **Akka.Streams Docs**: https://getakka.net/articles/streams/index.html -- **VitePress Docs**: https://vitepress.dev/ diff --git a/notes/Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION.md b/notes/Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION.md deleted file mode 100644 index 3ee366b73..000000000 --- a/notes/Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: ThreadPool Contention Resolution & ChannelExecutor Migration -date: '2026-04-03' -status: recommended -tags: - - dispatcher - - performance - - threadpool - - http2 - - akka-streams - - deadlock-prevention -related: - - Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS.md - - Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE.md ---- -# ThreadPool Contention Resolution & ChannelExecutor Migration - -## Problem Statement - -TurboHTTP's high-throughput HTTP/2 pipeline (64+ concurrent requests) experiences .NET ThreadPool contention, causing deadlocks in BenchmarkDotNet processes. The root cause is architectural: the default Akka.NET dispatcher (ThreadPoolDispatcher) shares the global .NET ThreadPool with application code, creating a circular dependency: - -1. Akka reserves ThreadPool threads to queue actor messages -2. GraphStage async I/O operations also queue to ThreadPool -3. BenchmarkDotNet harness waits for ThreadPool for its own Tasks -4. Contention → thread starvation → deadlock - -## Solution: Migrate to ChannelExecutor - -Implement ChannelExecutor as the default dispatcher. ChannelExecutor: -- Uses internal channel-based queue system instead of raw ThreadPool queuing -- Dynamically scales .NET ThreadPool based on actual demand -- Eliminates idle thread overhead (key advantage over ForkJoinDispatcher) -- Proven faster in Akka.NET benchmarks (5,200+ req/s vs 5,100 req/s for ForkJoinDispatcher) -- Available in Akka.NET 1.5.x (TurboHTTP uses 1.5.64 — fully supported) - -## Dispatcher Type Summary - -### Six Dispatcher Types in Akka.NET - -| Type | ThreadPool Use | Thread Management | HTTP/2 Suitability | Recommendation | -|------|----------------|-------------------|-------------------|----------------| -| **ThreadPoolDispatcher** | Global shared | None (TPL) | Poor (contention) | NO | -| **ForkJoinDispatcher** | Dedicated pool | Akka-owned, fixed count | Good | Alternative | -| **PinnedDispatcher** | Per-actor | One thread per actor | Terrible (too many threads) | NO | -| **SynchronizedDispatcher** | Context-dependent | SynchronizationContext | Not suitable (UI-only) | NO | -| **TaskDispatcher** | Global shared | TPL alternative | Poor (same as default) | NO | -| **ChannelExecutor** | Dynamic scaling | Akka with ThreadPool scaling | Excellent | YES ← **RECOMMENDED** | - -### Why ChannelExecutor Wins - -**Comparison: Default vs. ChannelExecutor** -- Default: Akka + app code compete for single ThreadPool → contention -- ChannelExecutor: Akka uses channel queues, scales ThreadPool dynamically → no contention - -**Comparison: ForkJoinDispatcher vs. ChannelExecutor** -- ForkJoinDispatcher: 32 dedicated threads always running (memory overhead) -- ChannelExecutor: 2-128 dynamic threads based on load (lower idle CPU) -- Result: ChannelExecutor faster + more memory-efficient - -**Performance Data** -- ThreadPoolDispatcher: ~4,800 req/s (baseline with contention) -- ForkJoinDispatcher: ~5,100 req/s (good, higher memory) -- ChannelExecutor: ~5,200+ req/s (fastest, lowest memory) - -## Implementation Plan - -### Files to Modify - -1. **`/src/TurboHTTP/TurboClientServiceCollectionExtensions.cs`** - - Add ChannelExecutor configuration to LoggingHocon - -2. **`/src/TurboHTTP.Benchmarks/StreamingThroughputBenchmarks.cs`** - - Add ChannelExecutor configuration to BenchHocon - -3. **`/src/TurboHTTP.IntegrationTests/Shared/ActorSystemFixture.cs`** (Optional) - - Add ChannelExecutor configuration for test ActorSystem - -### Configuration Template - -```hocon -akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } -} -``` - -**Parameters:** -- `executor = channel-executor` — Use ChannelExecutor instead of default -- `throughput = 30` — Process 30 messages per actor before yielding (balanced) -- `parallelism-min = 2` — Minimum threads (low to reduce startup overhead) -- `parallelism-factor = 2.0` — Max scaling = cores × 2.0 (2x per core for I/O-heavy) -- `parallelism-max = 128` — Hard cap on threads (prevents runaway growth) - -### Code Change Examples - -**Before (TurboClientServiceCollectionExtensions.cs):** -```csharp -private static readonly Config LoggingHocon = ConfigurationFactory.ParseString( - """akka.loggers = ["Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"]"""); -``` - -**After:** -```csharp -private static readonly Config LoggingHocon = ConfigurationFactory.ParseString( - """ - akka.loggers = ["Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"] - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - -**Before (StreamingThroughputBenchmarks.cs):** -```csharp -private static readonly Config BenchHocon = ConfigurationFactory.Empty; -``` - -**After:** -```csharp -private static readonly Config BenchHocon = ConfigurationFactory.ParseString( - """ - akka.actor.default-dispatcher = { - executor = channel-executor - throughput = 30 - fork-join-executor { - parallelism-min = 2 - parallelism-factor = 2.0 - parallelism-max = 128 - } - } - """); -``` - -## Expected Outcomes - -### Immediate (After Implementation) -1. No deadlocks in BenchmarkDotNet processes -2. ThreadPool remains available for application code -3. Stable latency across 64+ concurrent requests -4. 5-10% throughput improvement - -### Observable Improvements -- **Reduced idle CPU:** Dynamic scaling eliminates unused threads -- **Stable latency:** No ThreadPool contention spikes -- **Better cloud scaling:** Fewer idle threads in containerized environments -- **No memory regression:** ChannelExecutor uses less memory than ForkJoinDispatcher - -## Validation Steps - -### Phase 1: Compilation & Syntax -```bash -dotnet build --configuration Release ./src/TurboHTTP.sln -``` - -### Phase 2: Unit & Stream Tests -```bash -dotnet test --project TurboHTTP.Tests/TurboHTTP.Tests.csproj -dotnet test --project TurboHTTP.StreamTests/TurboHTTP.StreamTests.csproj -``` - -### Phase 3: Benchmark Validation -```bash -dotnet run --configuration Release --project TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj -``` -Expected: No hangs, timeouts, or deadlocks at any concurrency level (1, 4, 16, 64, 256). - -### Phase 4: Integration Tests -```bash -dotnet test --project TurboHTTP.IntegrationTests/TurboHTTP.IntegrationTests.csproj -``` -Expected: All HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3 tests pass. - -## Risk Assessment - -**Risk Level: VERY LOW** - -**Rationale:** -1. ChannelExecutor introduced in Akka.NET v1.4.19 (2022) -2. Production-tested for 2+ years across multiple organizations -3. Opt-in feature (not changing default framework behavior) -4. Configurable per-ActorSystem (isolated change) -5. Rollback trivial (revert configuration string) -6. No API changes required - -**Potential Issues & Mitigation:** -- Issue: Configuration not applied - - Mitigation: Verify config with `system.Settings.Config` logging - -- Issue: Increased memory usage - - Mitigation: Reduce `parallelism-factor` to 1.0 or lower `parallelism-max` - -- Issue: Different latency profile - - Mitigation: Adjust `throughput` parameter (10-50 range for tuning) - -## Configuration Variations by Environment - -### Development -```hocon -parallelism-factor = 1.0 -parallelism-max = 32 -throughput = 20 # More responsive -``` - -### Production (Cloud) -```hocon -parallelism-factor = 1.0 -parallelism-max = 64 -throughput = 30 -``` - -### Benchmarking (Maximum Throughput) -```hocon -parallelism-factor = 2.0 -parallelism-max = 128 -throughput = 30 -``` - -## Related Documentation - -- [[Architecture/Design/10-DISPATCHER_SELECTION_ANALYSIS|Dispatcher Selection Analysis]] — Complete analysis of all six dispatcher types -- [[Architecture/Guides/11-DISPATCHER_CONFIGURATION_GUIDE|Dispatcher Configuration Guide]] — Detailed configuration and tuning guide -- [[Architecture/Benchmarks/Benchmark_2026-04-03_Transport_Refactoring|Benchmark 2026-04-03]] — Current benchmark baseline - -## Success Criteria - -Implementation is successful if: -1. ✓ All benchmarks complete without hangs/deadlocks -2. ✓ Throughput maintained or improved (5,100+ req/s) -3. ✓ All integration tests pass (H10, H11, H2, H3, TLS) -4. ✓ Memory usage stable (compare before/after heap dumps) -5. ✓ CPU utilization consistent (no spikes from ThreadPool contention) -6. ✓ Latency variance reduced (P95 latency < P50 * 1.5) - -## Timeline - -- **Research & Analysis:** Complete (this note) -- **Implementation:** 2 config string changes (~15 minutes) -- **Testing & Validation:** ~30 minutes (benchmark + integration tests) -- **Total:** ~1 hour end-to-end - -## Conclusion - -ChannelExecutor is the optimal dispatcher for TurboHTTP's high-throughput HTTP/2 pipeline. It: -- Solves ThreadPool contention directly -- Improves performance over alternatives -- Requires minimal code changes -- Carries very low implementation risk -- Is production-ready (2+ years in field) - -**Recommendation:** Proceed with implementation immediately. diff --git a/notes/Architecture/Status/_INDEX.md b/notes/Architecture/Status/_INDEX.md deleted file mode 100644 index 26d8e5aca..000000000 --- a/notes/Architecture/Status/_INDEX.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Status Index -description: 'Index of project status notes — known gaps, current state, and roadmap' -tags: - - architecture - - status - - index ---- -# Status - -Project status, known gaps, and roadmap tracking for TurboHTTP. - -## Notes - -- [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps & Limitations]] — Critical issues, high-priority gaps, and recommended fixes before v1.0 -- [[Architecture/Status/04-CURRENT_STATE_SUMMARY|Current State Summary]] — Implementation status, completion scores by RFC, and next milestones -- [[Architecture/Status/12-THREADPOOL_CONTENTION_RESOLUTION|ThreadPool Contention Resolution]] — ChannelExecutor migration plan to eliminate ThreadPool starvation under HTTP/2 load diff --git a/notes/Features/Diagnostics/Feature009_Akka_Logging_Bridge.md b/notes/Features/Diagnostics/Feature009_Akka_Logging_Bridge.md deleted file mode 100644 index 66a3ab348..000000000 --- a/notes/Features/Diagnostics/Feature009_Akka_Logging_Bridge.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: "Feature 009: Akka Logging Bridge" -description: "Bridges Akka.NET internal logging to Microsoft.Extensions.Logging via Akka.Logger.Extensions.Logging" -tags: [features, history, logging, akka, infrastructure, hosting] -status: completed ---- - -# Feature 009: Akka Logging Bridge - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Infrastructure / Observability | -| **Scope** | 3 steps | - -## Description - -Integrated `Akka.Logger.Extensions.Logging` to bridge Akka.NET's internal actor system logging to the standard `Microsoft.Extensions.Logging` pipeline. This allowed Akka debug/info/error messages to appear in the same log output as ASP.NET Core and TurboHTTP application logs. - -- Added `Akka.Logger.Extensions.Logging` NuGet package -- Configured the logging bridge in the hosting layer (`TurboHttpServiceCollectionExtensions`) -- Added integration tests verifying Akka log messages flow through the bridge - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Hosting/TurboHttpServiceCollectionExtensions.cs` | DI configuration for logging bridge | -| `src/TurboHTTP.IntegrationTests/Diagnostics/AkkaLoggingBridgeTests.cs` | Bridge integration tests | - -## See Also - -- [[Features/Diagnostics/Feature010_Tracing_Infrastructure\|Feature 010]] — OTel tracing (built on top of logging) -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION\|Diagnostics Integration]] — full observability stack diff --git a/notes/Features/Diagnostics/Feature010_Tracing_Infrastructure.md b/notes/Features/Diagnostics/Feature010_Tracing_Infrastructure.md deleted file mode 100644 index 370aaaac9..000000000 --- a/notes/Features/Diagnostics/Feature010_Tracing_Infrastructure.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Feature 010: Tracing Infrastructure (TurboHttpInstrumentation)" -description: "OpenTelemetry ActivitySource distributed tracing wired into request lifecycle stages" -tags: [features, history, tracing, opentelemetry, diagnostics, instrumentation] -status: completed ---- - -# Feature 010: Tracing Infrastructure (TurboHttpInstrumentation) - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Observability / Diagnostics | -| **Scope** | 3 steps | - -## Description - -Added distributed tracing infrastructure using OpenTelemetry `ActivitySource`. The `TurboHttpInstrumentation` class became the central tracing entry point, emitting spans for request lifecycle events. - -- Added `TurboHttpInstrumentation` class with `ActivitySource` registration and span creation helpers -- Instrumented request lifecycle in pipeline stages — request start/end, encoding, decoding, connection acquisition -- Added unit tests verifying span creation, propagation, and correct parent/child relationships using `ActivityListener` - -Traces exposed via `TurboHttpInstrumentation.ActivitySourceName` for consumption by OTel collectors (Zipkin, OTLP, etc.). - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Diagnostics/TurboHttpInstrumentation.cs` | ActivitySource and span helpers | -| `src/TurboHTTP.Tests/Diagnostics/TurboHttpInstrumentationTests.cs` | Tracing unit tests | - -## See Also - -- [[Features/Diagnostics/Feature011_OTel_Metrics\|Feature 011]] — companion metrics infrastructure -- [[Features/Diagnostics/Feature012_Diagnostic_EventSource\|Feature 012]] — lower-level ETW/DiagnosticListener -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION\|Diagnostics Integration]] — full observability stack diff --git a/notes/Features/Diagnostics/Feature011_OTel_Metrics.md b/notes/Features/Diagnostics/Feature011_OTel_Metrics.md deleted file mode 100644 index 91c96ae7a..000000000 --- a/notes/Features/Diagnostics/Feature011_OTel_Metrics.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Feature 011: OpenTelemetry Metrics (TurboHttpMetrics)" -description: "OpenTelemetry Meter-based metrics for request counts, latency, and connection pool utilisation" -tags: [features, history, metrics, opentelemetry, diagnostics, instrumentation] -status: completed ---- - -# Feature 011: OpenTelemetry Metrics (TurboHttpMetrics) - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Observability / Diagnostics | -| **Scope** | 3 steps | - -## Description - -Added `System.Diagnostics.Metrics`-based metrics infrastructure using `TurboHttpMetrics`. Metrics were instrumented at the stage and pooling layers and exposed for consumption by OTel collectors and `dotnet-counters`. - -- Added `TurboHttpMetrics` with `Meter` registration, counters, histograms for request count, latency, bytes sent/received -- Instrumented pipeline stages and the connection pooling layer with metric recording calls -- Added unit tests using `MeterListener` to verify metric names, units, and values under load - -Metrics exposed under meter name `TurboHTTP` with instruments following .NET OTel naming conventions (`turbohttp.request.count`, `turbohttp.request.duration`, etc.). - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Diagnostics/TurboHttpMetrics.cs` | Meter and instrument definitions | -| `src/TurboHTTP.Tests/Diagnostics/TurboHttpMetricsTests.cs` | MeterListener-based unit tests | - -## See Also - -- [[Features/Diagnostics/Feature010_Tracing_Infrastructure\|Feature 010]] — companion distributed tracing -- [[Features/Diagnostics/Feature012_Diagnostic_EventSource\|Feature 012]] — lower-level ETW/EventSource -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION\|Diagnostics Integration]] — full observability stack diff --git a/notes/Features/Diagnostics/Feature012_Diagnostic_EventSource.md b/notes/Features/Diagnostics/Feature012_Diagnostic_EventSource.md deleted file mode 100644 index 9eef672b4..000000000 --- a/notes/Features/Diagnostics/Feature012_Diagnostic_EventSource.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Feature 012: DiagnosticListener and ETW EventSource Diagnostics" -description: "Low-level ETW EventSource and DiagnosticListener infrastructure for production diagnostics and tooling integration" -tags: [features, history, diagnostics, etw, eventsource, diagnosticlistener] -status: completed ---- - -# Feature 012: DiagnosticListener and ETW EventSource Diagnostics - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed (partially consolidated into TracingBidiStage in [[Features/Infrastructure/Feature016_TracingBidi_Consolidation\|Feature 016]]) | -| **Category** | Observability / Diagnostics | -| **Scope** | 4 steps | - -## Description - -Added low-level observability infrastructure using both ETW `EventSource` and the `DiagnosticListener` pattern — the same approach used by `HttpClient` and ASP.NET Core. - -- Added `TurboHttpEventSource` (`[EventSource(Name = "TurboHTTP")]`) for ETW/EventPipe diagnostics consumable by `dotnet-trace`, PerfView, and Application Insights -- Added `TurboHttpDiagnosticListener` for programmatic in-process event subscription (same pattern as `System.Net.Http` DiagnosticListener) -- Wired both into pipeline stages and the transport layer -- Added unit tests verifying EventSource event payloads and DiagnosticListener subscription/unsubscription - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Diagnostics/TurboHttpEventSource.cs` | ETW EventSource (later moved to TracingBidiStage) | -| `src/TurboHTTP/Diagnostics/TurboHttpDiagnosticListener.cs` | DiagnosticListener (later moved to TracingBidiStage) | -| `src/TurboHTTP.Tests/Diagnostics/DiagnosticsUnitTests.cs` | Diagnostic infrastructure tests | - -## See Also - -- [[Features/Infrastructure/Feature016_TracingBidi_Consolidation\|Feature 016]] — consolidated EventSource + DiagnosticListener into `TracingBidiStage` -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION\|Diagnostics Integration]] — full observability stack diff --git a/notes/Features/Diagnostics/_INDEX.md b/notes/Features/Diagnostics/_INDEX.md deleted file mode 100644 index 22fa659ab..000000000 --- a/notes/Features/Diagnostics/_INDEX.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Diagnostics Index -description: >- - Index of diagnostics feature notes — logging bridge, OTel tracing, metrics, - and ETW EventSource -tags: - - features - - diagnostics - - index ---- -# Diagnostics - -Observability and diagnostics features — logging, tracing, metrics, and ETW EventSource infrastructure. - -## Notes - -- [[Features/Diagnostics/Feature009_Akka_Logging_Bridge|Akka Logging Bridge]] — Bridges Akka.NET internal logging to Microsoft.Extensions.Logging -- [[Features/Diagnostics/Feature010_Tracing_Infrastructure|Tracing Infrastructure]] — OpenTelemetry ActivitySource distributed tracing wired into request lifecycle -- [[Features/Diagnostics/Feature011_OTel_Metrics|OTel Metrics]] — OpenTelemetry Meter-based metrics for request counts, latency, and connection pool utilisation -- [[Features/Diagnostics/Feature012_Diagnostic_EventSource|Diagnostic EventSource]] — Low-level ETW EventSource and DiagnosticListener for production diagnostics diff --git a/notes/Features/Infrastructure/Feature016_TracingBidi_Consolidation.md b/notes/Features/Infrastructure/Feature016_TracingBidi_Consolidation.md deleted file mode 100644 index fa5be0933..000000000 --- a/notes/Features/Infrastructure/Feature016_TracingBidi_Consolidation.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "Feature 016: TracingBidiStage Consolidation" -description: "Consolidated EventSource and DiagnosticListener from Diagnostics/ into a single TracingBidiStage, simplified HandlerBidiStage to pure pass-through" -tags: [features, history, tracing, refactoring, bidi-stage, architecture] -status: completed ---- - -# Feature 016: TracingBidiStage Consolidation - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Architecture Refactoring | -| **Scope** | 2 steps | - -## Description - -Consolidated the diagnostics infrastructure introduced in [[Features/Diagnostics/Feature012_Diagnostic_EventSource\|Feature 012]] into a dedicated pipeline stage, and simplified the handler bridge. - -- Moved `TurboHttpEventSource` and `TurboHttpDiagnosticListener` from the `Diagnostics/` folder into `TracingBidiStage` — a `GraphStage` that wraps the request/response flow and emits diagnostic events as data passes through. This aligned diagnostics with the stream-native architecture rather than side-effecting from external hooks. - -- Simplified `HandlerBidiStage` to a pure pass-through wrapper around `DelegatingHandler` — removed logic that had accumulated in the stage and pushed it into the handler chain where it belongs. The stage became a thin adapter between Akka.Streams and the `HttpMessageHandler` model. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs` | Consolidated diagnostics stage | -| `src/TurboHTTP/Streams/Stages/Features/HandlerBidiStage.cs` | Simplified handler bridge | - -## See Also - -- [[Features/Diagnostics/Feature012_Diagnostic_EventSource\|Feature 012]] — original diagnostics implementation -- [[Architecture/Layers/15-STREAMS_LAYER\|Streams Layer]] — stage composition and BidiFlow pipeline -- [[Architecture/Guides/17-DIAGNOSTICS_INTEGRATION\|Diagnostics Integration]] — observability stack diff --git a/notes/Features/Infrastructure/Feature018_Docs_Site_Revision.md b/notes/Features/Infrastructure/Feature018_Docs_Site_Revision.md deleted file mode 100644 index 1b89b8b6e..000000000 --- a/notes/Features/Infrastructure/Feature018_Docs_Site_Revision.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "Feature 018: Documentation Site Revision" -description: "User-goal-oriented rewrite of VitePress documentation site — guides, architecture diagrams, and LikeC4 diagram updates" -tags: [features, history, documentation, vitepress, likec4, guides] -status: completed ---- - -# Feature 018: Documentation Site Revision - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Documentation | -| **Scope** | 7 steps | - -## Description - -Comprehensive revision of the VitePress documentation site (`docs/`) to adopt user-goal-oriented language (what the user wants to accomplish, not what the library does internally). Updated all guide pages and architecture diagrams. - -| # | Scope | -|---|-------| -| 1 | `guide/redirects.md` — user-goal-oriented language | -| 2 | `guide/configuration.md`, `retries.md`, `caching.md`, `connection-pooling.md` | -| 3 | Split `guide/advanced.md` — Channel API to Getting Started; custom stages to Architecture | -| 4 | `architecture/pipeline.md` and `handlers.md` — goal-oriented language | -| 5 | Updated LikeC4 diagrams — renamed HTTP/2 stages, improved pipeline labels, added missing engine view stages | -| 6 | Site build verification, dead link detection, SVG fallback alignment | -| 7 | Final cross-reference check — all internal links resolve, no orphaned pages | - -The VitePress site uses Node.js 20+ and is served from `docs/`. Live reload via `npm run docs:dev`. - -## Key Source Files - -| File | Role | -|------|------| -| `docs/guide/` | All user-facing guide pages | -| `docs/architecture/` | Architecture documentation | -| `docs/.vitepress/` | VitePress configuration and theme | - -## See Also - -- [[Architecture/00-ONBOARDING\|Developer Onboarding Guide]] — internal developer docs (Obsidian vault) -- [[Architecture/Design/01-LAYERED_ARCHITECTURE\|Layered Architecture]] — architecture reference diff --git a/notes/Features/Infrastructure/Feature019_Stream_Survival.md b/notes/Features/Infrastructure/Feature019_Stream_Survival.md deleted file mode 100644 index 73cd05783..000000000 --- a/notes/Features/Infrastructure/Feature019_Stream_Survival.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: "Feature 019: Stream Survival — Error Absorption" -description: "Hardened all pipeline stages to absorb upstream failures rather than propagating them, preventing full stream teardown on individual request errors" -tags: [features, history, error-handling, akka-streams, resilience, bugfix] -status: completed ---- - -# Feature 019: Stream Survival — Error Absorption - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Resilience / Bug Fix | -| **Scope** | 6 steps | - -## Description - -Hardened the Akka.Streams pipeline so that individual request failures do not tear down the entire stream. Previously, if a stage received an upstream failure signal (e.g., connection write error), it propagated via Akka's default `onUpstreamFailure` behavior, killing the whole pipeline. This caused all in-flight requests to fail whenever a single connection error occurred. - -| # | Stage Fixed | -|---|------------| -| 1 | `ConnectionStage` — absorb outbound write failures (log + recover, not propagate) | -| 2 | `TracingBidiStage` — absorb upstream failure on response path | -| 3 | Correlation stages (`CorrelationHttp1XStage`, `CorrelationHttp20Stage`) — absorb upstream failures | -| 4 | Version router — block HTTP/3 with `NotSupportedException` instead of `FailStage` | -| 5 | `Http30ConnectionStage` — replace `FailStage` with log + absorb pattern | -| 6 | End-to-end verification — fixed final `FailStage` in `Http3ConnectionStage` | - -The fix pattern: override `onUpstreamFailure`, log the error, and call `CompleteStage()` rather than `FailStage(cause)`. This keeps downstream stages alive for subsequent requests. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/Routing/ConnectionStage.cs` | Outbound write failure absorption | -| `src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs` | Response path failure absorption | -| `src/TurboHTTP/Streams/Stages/Routing/CorrelationHttp1XStage.cs` | HTTP/1.x correlation absorption | -| `src/TurboHTTP/Streams/Stages/Routing/CorrelationHttp20Stage.cs` | HTTP/2 correlation absorption | - -## See Also - -- [[Features/Protocol/Feature017_ConnectionStage_Race\|Feature 017]] — related ConnectionStage fix -- [[Architecture/Layers/15-STREAMS_LAYER\|Streams Layer]] — stage error handling patterns -- [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS\|Known Gaps & Limitations]] — remaining stream lifecycle issues diff --git a/notes/Features/Infrastructure/Feature025_Clean_Protocol_Core.md b/notes/Features/Infrastructure/Feature025_Clean_Protocol_Core.md deleted file mode 100644 index 1c567fcce..000000000 --- a/notes/Features/Infrastructure/Feature025_Clean_Protocol_Core.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: "Feature 025: Clean Protocol Core — Single GroupByRequestKey" -description: "Invert the protocol-core topology so GroupByRequestKey is called once at the top level, with HTTP version routing and engine connection flows living inside each substream" -tags: [features, architecture, streams, protocol-core, refactoring] -status: planned ---- - -# Feature 025: Clean Protocol Core — Single GroupByRequestKey - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | 🟡 Planned | -| **Category** | Architecture Refactoring | -| **Scope** | 2 files (delete 1, rewrite 1) | - -## Problem - -`ProtocolCoreGraphBuilder` inverts the natural execution order. The current topology is: - -``` -Partition (by HTTP version) - ├─ GroupByRequestKey(256) → ConnectionFlow → MergeSubstreams - ├─ GroupByRequestKey(256) → ConnectionFlow → MergeSubstreams - ├─ GroupByRequestKey(64) → ConnectionFlow → MergeSubstreams - └─ GroupByRequestKey(64) → ConnectionFlow → MergeSubstreams -Merge -``` - -`GroupByRequestKey` is instantiated **four times** — once per HTTP version lane. The grouping key (`RequestEndpoint`) already contains the HTTP version, so the Partition and the per-lane GroupBy are doing redundant work at different levels of the graph. - -## Target Topology - -Invert: group first, then route by version inside each substream. - -``` -GroupByRequestKey(host:port:scheme:version, maxSubstreams=256) ← called once - └─ substream per endpoint (all requests have the same version) - Partition (by HTTP version) - ├─ ConnectionFlow - ├─ ConnectionFlow - ├─ ConnectionFlow - └─ ConnectionFlow - Merge -MergeSubstreams -``` - -Because `Version` is part of the `RequestEndpoint` key, every substream carries requests of exactly one HTTP version. The inner Partition always routes to a single branch — it is explicit rather than clever. - -## Design Decisions - -### Version stays in RequestEndpoint key - -`RequestEndpoint = (host, port, scheme, version)` is unchanged. Removing version from the key would be a semantic change: it would collapse HTTP/1.1 and HTTP/2 connections to the same host into one substream, which introduces mixed-version connection management complexity. The structural refactor is sufficient without changing semantics. - -### Single maxSubstreams = 256 - -Previously each HTTP version had its own GroupByRequestKey with a separate limit: - -| Version | Old limit | -|---------|-----------| -| HTTP/1.0 | 256 | -| HTTP/1.1 | 256 | -| HTTP/2 | 64 | -| HTTP/3 | 64 | - -With one GroupByRequestKey the limit is shared across all versions. `256` is used as the default — it matches the existing HTTP/1.x ceiling and is a reasonable upper bound for distinct endpoints. Because version is in the key, an HTTP/2 + HTTP/1.1 dual-stack host counts as two substreams, preserving relative separation. - -## Files - -| Action | File | -|--------|------| -| **Delete** | `src/TurboHTTP/Streams/ProtocolCoreGraphBuilder.cs` | -| **Rewrite** | `src/TurboHTTP/Streams/Engine.cs` | -| Keep | `src/TurboHTTP/Internal/RequestEndpoint.cs` | -| Keep | `src/TurboHTTP/Streams/Stages/Internal/GroupByRequestKeyStage.cs` | -| Keep | `src/TurboHTTP/Streams/Stages/Internal/HostKeyGroupByExtensions.cs` | -| Keep | `src/TurboHTTP.StreamTests/Streams/10_EngineVersionRoutingTests.cs` | - -## Implementation Sketch - -### `Engine.cs` changes - -Replace the `ProtocolCoreGraphBuilder.Build(...)` call in `BuildExtendedPipeline` with a call to a new private `BuildProtocolCore` method: - -```csharp -private static IGraph, NotUsed> - BuildProtocolCore( - ConnectionPool pool, - TurboClientOptions clientOptions, - Func>? http10Factory, - Func>? http11Factory, - Func>? http20Factory, - Func>? http30Factory) -{ - var http10 = BuildConnectionFlow(pool, http10Factory, clientOptions); - var http11 = BuildConnectionFlow(pool, http11Factory, clientOptions); - var http20 = BuildConnectionFlow(pool, http20Factory, clientOptions); - var http30 = BuildConnectionFlow(pool, http30Factory, clientOptions); - - var versionRouter = BuildVersionRouter(http10, http11, http20, http30); - var highThroughputBuffer = Attributes.CreateInputBuffer(16, 64); - - return (Flow) - Flow.Create() - .GroupByRequestKey(RequestEndpoint.FromRequest, maxSubstreams: 256) - .ViaSubFlow(versionRouter) - .MergeSubstreams() - .WithAttributes(highThroughputBuffer); -} - -private static IGraph, NotUsed> - BuildVersionRouter(/* four ConnectionFlow graphs */) -{ - return GraphDsl.Create(b => - { - var partition = b.Add(new Partition(4, msg - => msg.Version switch - { - { Major: 3, Minor: 0 } => 3, - { Major: 2, Minor: 0 } => 2, - { Major: 1, Minor: 1 } => 1, - { Major: 1, Minor: 0 } => 0, - _ => throw new ArgumentOutOfRangeException(...) - })); - - var merge = b.Add(new Merge(4)); - - b.From(partition.Out(0)).Via(b.Add(http10)).To(merge); - b.From(partition.Out(1)).Via(b.Add(http11)).To(merge); - b.From(partition.Out(2)).Via(b.Add(http20)).To(merge); - b.From(partition.Out(3)).Via(b.Add(http30)).To(merge); - - return new FlowShape(partition.In, merge.Out); - }); -} -``` - -`BuildConnectionFlow` moves from `ProtocolCoreGraphBuilder` into `Engine` unchanged. - -## Verification - -```bash -dotnet build --configuration Release ./src/TurboHTTP.sln - -dotnet test ./src/TurboHTTP.StreamTests/TurboHTTP.StreamTests.csproj \ - -- --filter-class "TurboHTTP.StreamTests.Streams.EngineVersionRoutingTests" - -dotnet test ./src/TurboHTTP.sln -``` - -## See Also - -- [[Architecture/Design/01-LAYERED_ARCHITECTURE|Layered Architecture]] — pipeline layer overview -- [[Architecture/Design/02-STAGE_PATTERNS|Stage Patterns]] — GraphStage conventions diff --git a/notes/Features/Infrastructure/_INDEX.md b/notes/Features/Infrastructure/_INDEX.md deleted file mode 100644 index a5f29dbf4..000000000 --- a/notes/Features/Infrastructure/_INDEX.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Infrastructure Index -description: >- - Index of infrastructure feature notes — refactoring, documentation, and - resilience hardening -tags: - - features - - infrastructure - - index ---- -# Infrastructure - -Infrastructure and cross-cutting features — refactoring, documentation, and resilience hardening. - -## Notes - -- [[Features/Infrastructure/Feature016_TracingBidi_Consolidation|TracingBidi Consolidation]] — Consolidated EventSource and DiagnosticListener into a single TracingBidiStage -- [[Features/Infrastructure/Feature018_Docs_Site_Revision|Docs Site Revision]] — User-goal-oriented rewrite of VitePress documentation site with LikeC4 diagram updates -- [[Features/Infrastructure/Feature019_Stream_Survival|Stream Survival]] — Hardened pipeline stages to absorb upstream failures rather than propagating full stream teardown -- [[Features/Infrastructure/Feature025_Clean_Protocol_Core|Clean Protocol Core]] — Invert protocol-core topology: one GroupByRequestKey at top, HTTP version routing inside each substream diff --git a/notes/Features/Performance/Feature024_Benchmark_Comparison.md b/notes/Features/Performance/Feature024_Benchmark_Comparison.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/notes/Features/Performance/_INDEX.md b/notes/Features/Performance/_INDEX.md deleted file mode 100644 index 18c00dea1..000000000 --- a/notes/Features/Performance/_INDEX.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Performance Index -description: Index of performance feature notes — benchmarking and optimisation -tags: - - features - - performance - - index ---- -# Performance - -Performance benchmarking and optimisation features. - -## Notes - -- [[Features/Performance/Feature024_Benchmark_Comparison|Benchmark Comparison]] — Performance benchmark comparison infrastructure diff --git a/notes/Features/Protocol/Feature003_Decompression_Stage.md b/notes/Features/Protocol/Feature003_Decompression_Stage.md deleted file mode 100644 index 6a33dc957..000000000 --- a/notes/Features/Protocol/Feature003_Decompression_Stage.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: "Feature 003: Decompression Stage" -description: "Initial HTTP response body decompression stage (RFC 9110 §8.4) — superseded by Feature 020" -tags: [features, history, streams, decompression, rfc9110] -status: completed ---- - -# Feature 003: Decompression Stage - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed (superseded by [[Features/Protocol/Feature020_ContentEncoding_Consolidation\|Feature 020]]) | -| **Category** | Pipeline Stage | -| **Scope** | Single commit | - -## Description - -Introduced `DecompressionStage`, an Akka.Streams `GraphStage>` that decompresses HTTP response bodies per RFC 9110 §8.4. The stage delegated to the existing `ContentEncodingDecoder` for gzip, x-gzip, deflate, and brotli (br) encodings. After decompression, it removed the `Content-Encoding` header and updated `Content-Length`. Responses with no `Content-Encoding` or `identity` encoding passed through unchanged. - -10 unit tests covered all supported encodings, header management, and multi-response scenarios. - -> **Note**: This stage was later renamed to `DecompressionBidiStage` and ultimately replaced by `ContentEncodingBidiStage` in [[Features/Protocol/Feature020_ContentEncoding_Consolidation\|Feature 020]], which consolidated all content-encoding logic into a single BidiFlow stage. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/DecompressionStage.cs` | Stage implementation (later removed) | -| `src/TurboHTTP.StreamTests/Streams/DecompressionStageTests.cs` | Unit tests | - -## See Also - -- [[Features/Protocol/Feature020_ContentEncoding_Consolidation\|Feature 020]] — supersedes this stage -- [[Architecture/Layers/15-STREAMS_LAYER\|Streams Layer]] — stage categories and composition diff --git a/notes/Features/Protocol/Feature004_HTTP10_Deadlock_Fix.md b/notes/Features/Protocol/Feature004_HTTP10_Deadlock_Fix.md deleted file mode 100644 index f8788739e..000000000 --- a/notes/Features/Protocol/Feature004_HTTP10_Deadlock_Fix.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Feature 004: HTTP/1.0 Demand Propagation Deadlock Fix" -description: "Fixed a permanent demand stall in ConnectionReuseStage for HTTP/1.0 pipelines" -tags: [features, history, http10, deadlock, akka-streams, bugfix] -status: completed ---- - -# Feature 004: HTTP/1.0 Demand Propagation Deadlock Fix - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Bug Fix | -| **Scope** | 3 steps | - -## Description - -HTTP/1.0 integration tests exhibited a critical deadlock that did not occur in HTTP/1.1 or HTTP/2.0. The root cause was in `ConnectionReuseStage.TryPullIfReady()`: the stage intentionally skips the control signal outlet for HTTP/1.0 (connection reuse does not apply per RFC 9110 §9.2.1), but `TryPullIfReady()` required demand from **both** outlets (response + signal) before pulling upstream. For HTTP/1.0, the signal outlet demand (`_signalOutletDemand`) never became `true`, causing a permanent demand stall — no new responses were ever requested. - -**Fix**: Gated the signal demand check on protocol version. For HTTP/1.0, `TryPullIfReady()` checks only `_responseOutletDemand` before pulling upstream (line 257: `if (!_isHttp10 && !_signalOutletDemand)`). This preserved the intentional signal-skip behaviour while unblocking upstream pulls. - -### Verification - -- All H10 integration tests completed without deadlock -- H11 (79/79) and H20 (72/72) showed zero regression -- 821/821 StreamTests passed - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/Routing/ConnectionReuseStage.cs` | Fix applied here (lines 225–278) | - -## See Also - -- [[Features/Testing/Feature005_H10_Flakiness_Mitigation\|Feature 005]] — follow-on flakiness mitigation -- [[Architecture/Design/02-STAGE_PATTERNS\|Stage Patterns]] — demand propagation and FanOutShape semantics diff --git a/notes/Features/Protocol/Feature017_ConnectionStage_Race.md b/notes/Features/Protocol/Feature017_ConnectionStage_Race.md deleted file mode 100644 index 3030c8434..000000000 --- a/notes/Features/Protocol/Feature017_ConnectionStage_Race.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: "Feature 017: ConnectionStage Race Condition Fix" -description: "Fixed ConnectionStage premature completion race and replaced Assert.Same with content equivalence in redirect tests" -tags: [features, history, bugfix, connection, race-condition, akka-streams] -status: completed ---- - -# Feature 017: ConnectionStage Race Condition Fix - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Bug Fix | -| **Scope** | 2 steps | - -## Description - -Fixed a race condition in `ConnectionStage` where stage completion could be triggered before the inbound pump had fully drained, and fixed a test fragility in redirect handler tests. - -- Replaced `Assert.Same` (reference equality) with content equivalence assertions in `RedirectHandler` tests. The original assertions were fragile because response objects could be recreated during redirect processing, causing false test failures even when content was identical. - -- Fixed the `ConnectionStage` race condition — deferred stage completion until the inbound response pump had fully drained. Previously, if the upstream completed while the inbound pump still had buffered data, the stage could complete prematurely and drop the final response bytes. Fix: tracked pump drain state explicitly and only called `CompleteStage()` once both conditions were satisfied. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/Routing/ConnectionStage.cs` | Race condition fix | -| `src/TurboHTTP.Tests/Features/RedirectHandlerTests.cs` | Test assertion fix | - -## See Also - -- [[Features/Infrastructure/Feature019_Stream_Survival\|Feature 019]] — related stream error absorption work -- [[Architecture/Layers/14-TRANSPORT_LAYER\|Transport Layer]] — connection lifecycle design diff --git a/notes/Features/Protocol/Feature020_ContentEncoding_Consolidation.md b/notes/Features/Protocol/Feature020_ContentEncoding_Consolidation.md deleted file mode 100644 index 9021dbed4..000000000 --- a/notes/Features/Protocol/Feature020_ContentEncoding_Consolidation.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: "Feature 020: ContentEncoding Architecture Consolidation" -description: "Consolidated scattered decompression logic from protocol decoders into a single ContentEncodingBidiStage at the stream layer" -tags: [features, history, architecture, refactoring, decompression, content-encoding, bidi-stage] -status: completed ---- - -# Feature 020: ContentEncoding Architecture Consolidation - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Architecture Refactoring | -| **Scope** | 5 steps | - -## Description - -Consolidated all HTTP response body decompression logic — which had accumulated in three separate protocol decoders and the original `DecompressionBidiStage` — into a single `ContentEncodingBidiStage` at the Streams layer. This gave decompression a single, well-tested, stream-native home. - -| # | Change | -|---|--------| -| 1 | Removed decompression from `Http10Decoder` and `Http11Decoder` — they now pass `Content-Encoding` headers through unchanged | -| 2 | Removed decompression from `Http20StreamStage` | -| 3 | Removed decompression from `Http30StreamStage` | -| 4 | Renamed all references from `DecompressionBidiStage` → `ContentEncodingBidiStage`; updated pipeline wiring in `ProtocolCoreGraphBuilder` | -| 5 | End-to-end verification — all compression integration tests pass after consolidation | - -**Before**: Decompression scattered across Http10Decoder, Http11Decoder, Http20StreamStage, Http30StreamStage, and DecompressionBidiStage. -**After**: Single `ContentEncodingBidiStage` handles all encodings (gzip, deflate, brotli, identity, unknown pass-through). - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP/Streams/Stages/Features/ContentEncodingBidiStage.cs` | Consolidated decompression stage | -| `src/TurboHTTP/Streams/Routing/ProtocolCoreGraphBuilder.cs` | Pipeline wiring updated | -| `src/TurboHTTP/Protocol/RFC9110/Http10Decoder.cs` | Decompression removed | -| `src/TurboHTTP/Protocol/RFC9113/Http20StreamStage.cs` | Decompression removed | - -## See Also - -- [[Features/Protocol/Feature003_Decompression_Stage|Feature 003]] — original `DecompressionStage` (superseded by this) -- [[Architecture/Layers/15-STREAMS_LAYER|Streams Layer]] — stage layer responsibilities -- [[Architecture/Layers/16-PROTOCOL_LAYER|Protocol Layer]] — what remains in protocol decoders after this refactor diff --git a/notes/Features/Protocol/_INDEX.md b/notes/Features/Protocol/_INDEX.md deleted file mode 100644 index eaa7a6080..000000000 --- a/notes/Features/Protocol/_INDEX.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Protocol Index -description: >- - Index of protocol-level feature notes — bug fixes, stage implementations, and - architectural changes -tags: - - features - - protocol - - index ---- -# Protocol - -Protocol-level features — bug fixes, stage implementations, and architectural changes in the HTTP pipeline. - -## Notes - -- [[Features/Protocol/Feature003_Decompression_Stage|Decompression Stage]] — Initial HTTP response body decompression stage (superseded by Feature 020) -- [[Features/Protocol/Feature004_HTTP10_Deadlock_Fix|HTTP/1.0 Deadlock Fix]] — Fixed permanent demand stall in ConnectionReuseStage for HTTP/1.0 pipelines -- [[Features/Protocol/Feature017_ConnectionStage_Race|ConnectionStage Race Fix]] — Fixed premature completion race in ConnectionStage and redirect test fragility -- [[Features/Protocol/Feature020_ContentEncoding_Consolidation|ContentEncoding Consolidation]] — Consolidated scattered decompression logic into a single ContentEncodingBidiStage diff --git a/notes/Features/Testing/Feature005_H10_Flakiness_Mitigation.md b/notes/Features/Testing/Feature005_H10_Flakiness_Mitigation.md deleted file mode 100644 index 40c175f90..000000000 --- a/notes/Features/Testing/Feature005_H10_Flakiness_Mitigation.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "Feature 005: HTTP/1.0 Integration Test Flakiness Mitigation" -description: "Three-phase mitigation of HTTP/1.0 test timeout failures caused by TCP connection churn and fixture contention" -tags: [features, history, http10, testing, flakiness, infrastructure] -status: in-progress ---- - -# Feature 005: HTTP/1.0 Integration Test Flakiness Mitigation - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | 🔶 In Progress (Phase 1 partially complete) | -| **Category** | Test Infrastructure | -| **Scope** | 9 steps across 3 phases | - -## Description - -After the [[Features/Protocol/Feature004_HTTP10_Deadlock_Fix\|Feature 004 deadlock fix]], the H10 integration suite still showed 6–9 timeout failures per 88-test run (~10% failure rate). These were **not deadlocks** but resource contention timeouts caused by: - -- **TCP connection churn** — HTTP/1.0 closes connections per response; 192 TCP connections across the suite with 100ms overhead each -- **Shared fixture bottleneck** — Single `ServerFixture` + `ActorSystemFixture` for all H10 tests -- **Timeout mismatch** — 10s inner timeout vs 30s outer timeout left thin margins under GC pauses -- **Actor system thread pool starvation** — Cleanup messages draining the pool between tests -- **Blocking routes** — `/delay/10000` (10-second block) in `ErrorHandlingIntegrationTests` monopolizing Kestrel - -### Three-Phase Mitigation Plan - -| Phase | Changes | Target | -|-------|---------|--------| -| Phase 1 | Timeout 10s→15s, explicit `DisposeAsync()`, isolate `ErrorHandlingIntegrationTests` | <2 timeouts/run | -| Phase 2 | Parallelise collections, tune ActorSystem thread pool (8→16 threads) | <1 timeout/run | -| Phase 3 | Dedicated fixtures for `RedirectIntegrationTests`, `RetryIntegrationTests` | 0 timeouts/run | - -**Phase 1 status**: Timeout increase and explicit cleanup steps completed. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.IntegrationTests/H10/` | 10 affected test classes (88 tests total) | -| `src/TurboHTTP.IntegrationTests/Shared/` | `ActorSystemFixture`, `ServerFixture` | - -## See Also - -- [[Features/Protocol/Feature004_HTTP10_Deadlock_Fix\|Feature 004]] — prerequisite deadlock fix -- [[Architecture/Guides/12-TEST_ORGANIZATION\|Test Organization]] — collection structure and fixture patterns diff --git a/notes/Features/Testing/Feature006_Connection_Management_Tests.md b/notes/Features/Testing/Feature006_Connection_Management_Tests.md deleted file mode 100644 index af82034f8..000000000 --- a/notes/Features/Testing/Feature006_Connection_Management_Tests.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Feature 006: HTTP/1.1 Connection Management Integration Tests" -description: "Integration test coverage for HTTP/1.1 connection keep-alive, pipelining, and lifecycle behaviour" -tags: [features, history, http11, testing, connection-management] -status: completed ---- - -# Feature 006: HTTP/1.1 Connection Management Integration Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Integration Tests | -| **Scope** | Single step | - -## Description - -Added integration tests for HTTP/1.1 connection management behaviour, covering keep-alive semantics, connection lifecycle, and persistent connection reuse. These tests verified that the `ConnectionReuseStage` correctly managed HTTP/1.1 keep-alive connections under real network conditions using the `KestrelFixture` test server. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.IntegrationTests/H11/ConnectionIntegrationTests.cs` | Connection management tests | -| `src/TurboHTTP.IntegrationTests/Shared/Routes.cs` | Test server routes | - -## See Also - -- [[Architecture/Layers/14-TRANSPORT_LAYER\|Transport Layer]] — connection pool and keep-alive design -- [[Architecture/Layers/15-STREAMS_LAYER\|Streams Layer]] — `ConnectionReuseStage` role in pipeline diff --git a/notes/Features/Testing/Feature007_Error_Handling_Tests.md b/notes/Features/Testing/Feature007_Error_Handling_Tests.md deleted file mode 100644 index cde20f65a..000000000 --- a/notes/Features/Testing/Feature007_Error_Handling_Tests.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Feature 007: Error Handling Integration Tests" -description: "Integration test coverage for HTTP/1.1 and HTTP/2 error handling, status codes, and failure scenarios" -tags: [features, history, http11, http2, testing, error-handling] -status: completed ---- - -# Feature 007: Error Handling Integration Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Integration Tests | -| **Scope** | 3 steps | - -## Description - -Added integration tests covering error handling across HTTP/1.1 and HTTP/2, including: - -- HTTP/1.1 error handling — 4xx/5xx responses, malformed responses, server disconnects -- HTTP/2 error handling — stream errors, GOAWAY frames, RST_STREAM handling -- Full suite verification — no regressions across both protocol versions - -Tests used a dedicated `/error/` route family on the `KestrelFixture` server to trigger controlled failure scenarios. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.IntegrationTests/H11/ErrorHandlingIntegrationTests.cs` | HTTP/1.1 error tests | -| `src/TurboHTTP.IntegrationTests/H20/ErrorHandlingH2IntegrationTests.cs` | HTTP/2 error tests | - -## See Also - -- [[Features/Infrastructure/Feature019_Stream_Survival\|Feature 019]] — later stream error absorption work -- [[Architecture/Layers/15-STREAMS_LAYER\|Streams Layer]] — stage error handling patterns diff --git a/notes/Features/Testing/Feature008_TLS_Integration_Tests.md b/notes/Features/Testing/Feature008_TLS_Integration_Tests.md deleted file mode 100644 index 34de4e798..000000000 --- a/notes/Features/Testing/Feature008_TLS_Integration_Tests.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Feature 008: TLS Integration Tests" -description: "Integration test coverage for HTTPS/TLS connections using the Kestrel TLS fixture" -tags: [features, history, tls, https, testing, security] -status: completed ---- - -# Feature 008: TLS Integration Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Integration Tests | -| **Scope** | Single step | - -## Description - -Added integration tests for HTTPS/TLS connections using the `KestrelTlsFixture`. Tests verified: - -- TLS handshake and certificate negotiation -- HTTPS request/response round-trips (HTTP/1.1 over TLS) -- HTTP/2 over TLS (ALPN negotiation) -- Basic cipher and protocol version behaviour - -The `KestrelTlsFixture` spins up a Kestrel server with a self-signed dev certificate. Client-side TLS was configured through `TurboHttpClientBuilder` with certificate validation bypass for test environments. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.IntegrationTests/Tls/TlsIntegrationTests.cs` | TLS integration tests | -| `src/TurboHTTP.IntegrationTests/Shared/KestrelTlsFixture.cs` | TLS server fixture | - -## See Also - -- [[Features/Testing/Feature013_Security_Tests\|Feature 013]] — security-focused adversarial tests -- [[Architecture/Layers/14-TRANSPORT_LAYER\|Transport Layer]] — TCP/TLS transport design diff --git a/notes/Features/Testing/Feature013_Security_Tests.md b/notes/Features/Testing/Feature013_Security_Tests.md deleted file mode 100644 index 149bf6d90..000000000 --- a/notes/Features/Testing/Feature013_Security_Tests.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "Feature 013: Security Tests" -description: "Adversarial security test suite covering header injection, request smuggling, cookie security, URI traversal, and HPACK attacks" -tags: [features, history, security, testing, hpack, http-smuggling] -status: completed ---- - -# Feature 013: Security Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Security / Testing | -| **Scope** | 5 steps | - -## Description - -Added a comprehensive adversarial security test suite targeting HTTP protocol attack vectors. Tests verified that TurboHTTP correctly rejects or handles malicious inputs across all protocol layers. - -| # | Coverage | -|---|----------| -| 1 | Header injection and HTTP request smuggling (RFC 9112 §11.2) | -| 2 | TLS transport security — weak ciphers, expired certs, MITM scenarios | -| 3 | Cookie security — `HttpOnly`, `Secure`, `SameSite`, injection attempts | -| 4 | URI sanitization and path traversal (`../` sequences, null bytes, encoded separators) | -| 5 | HPACK bomb attacks (highly compressed headers), protocol abuse (oversized frames, invalid stream IDs) | - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.Tests/Security/HeaderSecurityTests.cs` | Header injection and smuggling | -| `src/TurboHTTP.Tests/Security/TlsSecurityTests.cs` | Transport security | -| `src/TurboHTTP.Tests/Security/CookieSecurityTests.cs` | Cookie attack surface | -| `src/TurboHTTP.Tests/Security/UriSecurityTests.cs` | URI sanitization | -| `src/TurboHTTP.Tests/Security/HpackSecurityTests.cs` | HPACK bomb and protocol abuse | - -## See Also - -- [[Features/Testing/Feature015_H2_HPACK_Fuzzing\|Feature 015]] — related HPACK adversarial fuzzing -- [[Architecture/Layers/16-PROTOCOL_LAYER\|Protocol Layer]] — HPACK/QPACK internals diff --git a/notes/Features/Testing/Feature014_Decoder_Fuzzing.md b/notes/Features/Testing/Feature014_Decoder_Fuzzing.md deleted file mode 100644 index 6f7ee0a3b..000000000 --- a/notes/Features/Testing/Feature014_Decoder_Fuzzing.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Feature 014: HTTP/1.0 and HTTP/1.1 Decoder Fuzzing Tests" -description: "Adversarial fuzzing tests for HTTP/1.0 and HTTP/1.1 decoders covering malformed input, truncated frames, and boundary conditions" -tags: [features, history, fuzzing, testing, http10, http11, decoder] -status: completed ---- - -# Feature 014: HTTP/1.0 and HTTP/1.1 Decoder Fuzzing Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Testing / Robustness | -| **Scope** | 2 steps | - -## Description - -Added adversarial fuzzing tests for the HTTP/1.x decoder stages, verifying correct handling of malformed and edge-case inputs without panics, hangs, or incorrect output. - -- HTTP/1.0 decoder fuzzing — malformed status lines, missing headers, truncated bodies, invalid content-length values, non-UTF8 header values -- HTTP/1.1 decoder fuzzing — invalid chunk encoding, invalid transfer-encoding combinations, header field limit violations, pipeline request boundary errors - -All fuzz inputs were crafted as deterministic test cases (not property-based) following the RFC 9112 §11 security considerations section. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.Tests/RFC1945/Http10DecoderFuzzingTests.cs` | HTTP/1.0 decoder fuzz cases | -| `src/TurboHTTP.Tests/RFC9112/Http11DecoderFuzzingTests.cs` | HTTP/1.1 decoder fuzz cases | - -## See Also - -- [[Features/Testing/Feature015_H2_HPACK_Fuzzing\|Feature 015]] — companion HTTP/2 and HPACK fuzzing -- [[Architecture/Layers/16-PROTOCOL_LAYER\|Protocol Layer]] — decoder pipeline architecture -- [[Architecture/Design/06-DECODER_PIPELINE_ARCHITECTURE\|Decoder Pipeline Architecture]] — three-layer decoder design diff --git a/notes/Features/Testing/Feature015_H2_HPACK_Fuzzing.md b/notes/Features/Testing/Feature015_H2_HPACK_Fuzzing.md deleted file mode 100644 index d91876e3a..000000000 --- a/notes/Features/Testing/Feature015_H2_HPACK_Fuzzing.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Feature 015: HTTP/2 Frame and HPACK Adversarial Fuzzing Tests" -description: "Adversarial fuzzing for HTTP/2 frame parser and HPACK decoder covering malformed frames and compression attacks" -tags: [features, history, fuzzing, testing, http2, hpack, decoder] -status: completed ---- - -# Feature 015: HTTP/2 Frame and HPACK Adversarial Fuzzing Tests - -## Summary - -| Field | Value | -|-------|-------| -| **Status** | ✅ Completed | -| **Category** | Testing / Robustness | -| **Scope** | 2 steps | - -## Description - -Extended the fuzzing test coverage to HTTP/2 frame parsing and HPACK header compression, complementing the HTTP/1.x fuzzing from [[Features/Testing/Feature014_Decoder_Fuzzing\|Feature 014]]. - -- HTTP/2 frame parser adversarial fuzzing — invalid frame types, wrong payload lengths, frames on invalid stream IDs, reserved bit violations (RFC 9113 §4.1) -- HPACK decoder adversarial fuzzing — Huffman decoding errors, integer representation overflows, invalid index table references, header list size violations (RFC 7541) - -Tests complemented the security tests from [[Features/Testing/Feature013_Security_Tests\|Feature 013]] (HPACK bomb), focusing more on parser correctness than attack-specific scenarios. - -## Key Source Files - -| File | Role | -|------|------| -| `src/TurboHTTP.Tests/RFC9113/Http20FrameParserFuzzingTests.cs` | HTTP/2 frame parser fuzz cases | -| `src/TurboHTTP.Tests/RFC9113/HpackDecoderFuzzingTests.cs` | HPACK decoder adversarial tests | - -## See Also - -- [[Features/Testing/Feature014_Decoder_Fuzzing\|Feature 014]] — HTTP/1.x decoder fuzzing -- [[Features/Testing/Feature013_Security_Tests\|Feature 013]] — security-focused adversarial tests -- [[Architecture/Layers/16-PROTOCOL_LAYER\|Protocol Layer]] — HPACK internals diff --git a/notes/Features/Testing/_INDEX.md b/notes/Features/Testing/_INDEX.md deleted file mode 100644 index 6216db9ca..000000000 --- a/notes/Features/Testing/_INDEX.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Testing Index -description: >- - Index of testing feature notes — integration tests, fuzzing, security tests, - and flakiness mitigation -tags: - - features - - testing - - index ---- -# Testing - -Test infrastructure and test coverage features — integration tests, fuzzing, security, and flakiness mitigation. - -## Notes - -- [[Features/Testing/Feature005_H10_Flakiness_Mitigation|H10 Flakiness Mitigation]] — Three-phase mitigation of HTTP/1.0 test timeout failures caused by TCP connection churn -- [[Features/Testing/Feature006_Connection_Management_Tests|Connection Management Tests]] — Integration tests for HTTP/1.1 connection keep-alive, pipelining, and lifecycle -- [[Features/Testing/Feature007_Error_Handling_Tests|Error Handling Tests]] — Integration tests for HTTP/1.1 and HTTP/2 error handling and failure scenarios -- [[Features/Testing/Feature008_TLS_Integration_Tests|TLS Integration Tests]] — HTTPS/TLS connection testing using Kestrel TLS fixture -- [[Features/Testing/Feature013_Security_Tests|Security Tests]] — Adversarial security suite covering header injection, smuggling, cookie security, and HPACK attacks -- [[Features/Testing/Feature014_Decoder_Fuzzing|Decoder Fuzzing]] — Adversarial fuzzing for HTTP/1.0 and HTTP/1.1 decoders covering malformed input and boundary conditions -- [[Features/Testing/Feature015_H2_HPACK_Fuzzing|H2 HPACK Fuzzing]] — Adversarial fuzzing for HTTP/2 frame parser and HPACK decoder diff --git a/notes/RFC/00-RFC_STATUS_MATRIX.md b/notes/RFC/00-RFC_STATUS_MATRIX.md deleted file mode 100644 index fa72ac323..000000000 --- a/notes/RFC/00-RFC_STATUS_MATRIX.md +++ /dev/null @@ -1,308 +0,0 @@ -# RFC Compliance Status Matrix - -**Last Updated**: 2026-03-28 -**Overall Client-Side Compliance**: 86/100 — Production-Ready -**Test Coverage**: 260+ unit tests, 515+ integration tests - -## Summary by RFC - -| RFC | Standard | Status | Client Score | Server | Notes | -|-----|----------|--------|--------------|--------|-------| -| **RFC 1945** | HTTP/1.0 | ✅ Complete | 85/100 | ❌ None | Basic HTTP, no keep-alive, one request per connection | -| **RFC 9112** | HTTP/1.1 | ✅ Excellent | 92/100 | ❌ None | Modern RFC replacing RFC 7230-7235, message framing, connection management | -| **RFC 9113** | HTTP/2 | ✅ Very Thorough | 87/100 | ❌ None | Binary framing, multiplexing, flow control, stream priorities | -| **RFC 7541** | HPACK | ✅ Complete | 90/100 | ❌ None | Header compression for HTTP/2, dynamic table, Huffman coding | -| **RFC 9114** | HTTP/3 | 🔶 Partial | 60/100 | ❌ None | HTTP over QUIC, variable-length frames, stream types (encoder/decoder partially done) | -| **RFC 9000** | QUIC | 🔶 Partial | 50/100 | ❌ None | QUIC transport, variable-length integers, packet structure (primitives only) | -| **RFC 9204** | QPACK | ✅ Complete | 90/100 | ❌ None | Header compression for HTTP/3, dynamic table, Huffman coding | -| **RFC 9110** | HTTP Semantics | ✅ Good | 82/100 | ❌ None | Redirects (301/302/303/307/308), retries, content negotiation, method semantics | -| **RFC 6265** | Cookies | ✅ Good | 80/100 | ❌ None | Domain/path matching, Secure/HttpOnly/SameSite, Max-Age/Expires | -| **RFC 9111** | Caching | ✅ Good | 78/100 | ❌ None | Freshness, validation, storage, Cache-Control directives | - -## Detailed Compliance by Component - -### RFC 1945 (HTTP/1.0) — 85/100 - -**Implemented** ✅: -- Request-line parsing (METHOD URI HTTP-VERSION) -- General headers (Date, Via, Warning, Connection) -- Entity headers (Content-Length, Content-Type, Content-Encoding, Last-Modified, Expires) -- One request per connection (no pipelining) -- Simple string body boundaries (Content-Length or EOF) - -**Gaps** 🔶: -- No streaming request encoding (buffered only) -- No header limit validation (DoS protection) -- No connection reuse optimization - -**Test Files**: -- `TurboHTTP.Tests/RFC1945/` — 17 test classes, 233 unit tests -- `TurboHTTP.StreamTests/RFC1945/` — encoder/decoder stage tests, TCP fragmentation - -### RFC 9112 (HTTP/1.1) — 92/100 - -**Implemented** ✅: -- Request-line with Host header (required) -- Request headers (User-Agent, Accept, Accept-Encoding, etc.) -- Chunked Transfer-Encoding (RFC 9112 §6.1) -- Content-Length validation -- Keep-Alive / Connection close semantics -- HTTP/1.0 interop (no keep-alive unless `Connection: Keep-Alive`) -- Pipelining support (multiple requests per connection) -- CRLF line endings, header case-insensitivity - -**Gaps** 🔶: -- No chunk extensions (RFC 9112 §6.1 — rarely used) -- No trailer headers (rarely used) -- Limited strictness on obsolete-text headers - -**Test Files**: -- `TurboHTTP.Tests/RFC9112/` — 26 test classes, 374 unit tests -- `TurboHTTP.StreamTests/RFC9112/` — encoder/decoder/chunked/correlation/pipeline stages - -### RFC 9113 (HTTP/2) — 87/100 - -**Implemented** ✅: -- Connection preface ("PRI * HTTP/2.0\r\n...") -- Frame types: DATA, HEADERS, CONTINUATION, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, RST_STREAM -- Stream state machine (idle → open → closed) -- Flow control (WINDOW_UPDATE, stream window, connection window) -- Priority system (depends-on, weight, exclusive flag) -- Multiplexing (multiple streams per connection) -- Pseudo-headers validation (`:method`, `:scheme`, `:authority`, `:path`) -- HPACK header compression -- Server push (push promise parsing) -- Connection preface validation - -**Gaps** 🔶: -- No MAX_CONCURRENT_STREAMS validation in client (not enforced) -- No SETTINGS acknowledgment (auto-sent but not tracked) -- Limited stream priority handling (ignored in routing) -- No alternate service (Alt-Svc) handling - -**Test Files**: -- `TurboHTTP.Tests/RFC9113/` — 27 test classes, 545 unit tests -- `TurboHTTP.StreamTests/RFC9113/` — encoder/decoder/connection/stream/HPACK/correlation - -### RFC 7541 (HPACK) — 90/100 - -**Implemented** ✅: -- Dynamic table (4KB default, configurable) -- Static table (61 entries RFC 7541 Appendix B) -- Literal representation (indexed, literal w/ incremental, literal w/o indexing, literal never-indexed) -- Huffman encoding/decoding -- Sensitive header handling (Authorization, Cookie → never-indexed automatically) -- Eviction policy (FIFO with size management) -- Max table size dynamic updates -- Reference tracking (absolute + relative indexing) - -**Gaps** 🔶: -- No bounds checking on large headers (DoS vector) -- No header count limits (could exhaust memory) -- Limited error recovery on corrupted tables - -**Test Files**: -- `TurboHTTP.Tests/RFC7541/` — 7 test classes, 419 unit tests -- `TurboHTTP.StreamTests/RFC7541/` — HPACK stream integration - -### RFC 9114 (HTTP/3) — 60/100 - -**Implemented** 🔶: -- Frame types: DATA, HEADERS, CANCEL_PUSH, SETTINGS, PUSH_PROMISE, GOAWAY, MAX_PUSH_ID -- Variable-length frame headers (QUIC integers) -- Stream types (control, request, push promise, unidirectional) -- Settings frame parsing -- Pseudo-headers (same as HTTP/2) -- Field validation (header name/value format) -- Origin validation (for multi-origin requests) - -**NOT Implemented** ❌: -- Server push acceptance (push promise handling is minimal) -- Datagram extension (RFC 9297) -- Request forgetting (CANCEL_PUSH) -- Field section timeout -- Protocol error handling (detailed error codes) -- Most advanced flow control semantics - -**Test Files**: -- `TurboHTTP.Tests/RFC9114/` — Exists but minimal coverage -- `TurboHTTP.StreamTests/RFC9114/` — Partial encoder/decoder stubs - -### RFC 9000 (QUIC) — 50/100 - -**Implemented** 🔶: -- Variable-length integer encoding/decoding (QuicVarInt) -- Long form packet headers (basics only) -- Handshake, Initial, Retry packet types (parsing only) -- Connection ID handling (opaque, no validation) - -**NOT Implemented** ❌: -- Packet number space management -- Loss detection and congestion control -- Connection migration -- Stateless reset -- Key update -- Connection close -- Datagram frames -- Stream frame structure (left to HTTP/3) - -**Test Files**: -- `TurboHTTP.Tests/RFC9114/` — QUIC integer tests only -- Actual QUIC implementation is in TurboHTTP.Transport.Quic (if exists) - -### RFC 9204 (QPACK) — 90/100 - -**Implemented** ✅: -- Encoder with dynamic table management -- Decoder with blocking references -- Static table (61 entries, same as HPACK) -- Dynamic table (streamed updates via separate decoder stream) -- Variable-length integer encoding for indices -- Huffman encoding/decoding -- Sensitive header handling - -**Gaps** 🔶: -- No bounds checking on large headers (DoS vector) -- No header count limits (could exhaust memory) -- Limited error recovery on corrupted tables - -**Test Note**: QPACK encoder/decoder fully implemented with all core features. - -**Test Files**: -- `TurboHTTP.Tests/RFC9204/` — 11 test classes, 180+ unit tests -- `TurboHTTP.StreamTests/RFC9204/` — Encoder/decoder stage tests - -### RFC 9110 (HTTP Semantics) — 82/100 - -**Implemented** ✅: -- **Redirects** (RFC 9110 §15.4) — 301, 302, 303, 307, 308 with correct method rewriting -- **Idempotent Retry** (RFC 9110 §9.2) — Retry-After parsing, exponential backoff -- **Content Negotiation** (RFC 9110 §12) — Accept, Content-Type, Content-Encoding matching -- **Method Semantics** — GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, TRACE semantics -- **Status Codes** — 1xx, 2xx, 3xx, 4xx, 5xx handling -- **Request Target** — origin-form, absolute-form, authority-form, asterisk-form - -**Gaps** 🔶: -- No HTTPS→HTTP protection (redirect security) -- No loop detection (prevents infinite redirect chains) -- Limited content negotiation (server-driven only) - -**Test Files**: -- `TurboHTTP.Tests/RFC9110/` — 2 test classes (small, should expand) -- `TurboHTTP.StreamTests/RFC9110/` — Redirect, retry, decompression stages - -### RFC 6265 (Cookies) — 80/100 - -**Implemented** ✅: -- Cookie parsing (Set-Cookie header) -- Domain matching (exact, prefix with leading dot) -- Path matching (default, exact, prefix) -- Expires parsing (RFC 1123 date) -- Max-Age handling (overrides Expires) -- Secure flag (HTTPS only) -- HttpOnly flag (no JavaScript access) -- SameSite attribute (Strict, Lax, None) -- Cookie jar storage (thread-safe, LRU with TTL) -- Request cookie injection (Cookie header) - -**Gaps** 🔶: -- No public suffix list (bare domains treated as public) -- No third-party cookie blocking (all cookies accepted) -- No IP address handling (domain matching only) -- Limited origin validation - -**Test Files**: -- `TurboHTTP.Tests/RFC6265/` — 2 test classes, 66 unit tests -- `TurboHTTP.StreamTests/RFC6265/` — Cookie injection/storage stages - -### RFC 9111 (Caching) — 78/100 - -**Implemented** ✅: -- **Freshness** (RFC 9111 §4.2) — Cache-Control max-age, Expires, s-maxage -- **Validation** (RFC 9111 §4.3) — Conditional requests (If-None-Match, If-Modified-Since), 304 merge -- **Storage** — In-memory LRU cache with Vary support -- **Cache-Control** directives — public, private, no-cache, no-store, max-age, s-maxage -- **Entity Tags** (ETag) — weak and strong validation -- **Last-Modified** — RFC 9110 date-based validation - -**Gaps** 🔶: -- No shared cache (only private cache) -- No pragma: no-cache support (legacy) -- No heuristic freshness (rarely needed) -- No cache key normalization (fragment handling) -- Limited cache invalidation on POST/PUT/DELETE - -**Test Files**: -- `TurboHTTP.Tests/RFC9111/` — 4 test classes, 75 unit tests -- `TurboHTTP.StreamTests/RFC9111/` — Cache lookup/storage stages - -## Section-Level Compliance Documentation - -Each core RFC now has ≥8 section files with detailed `TurboHTTP Compliance` blocks documenting implementation status, key components, compliance details, gaps, and test references. - -| RFC | Total Section Files | Files with Compliance Docs | Key Sections Covered | -|-----|--------------------|-----------------------------|----------------------| -| **RFC 9110** | 8 | 8 | §6.1 Framing, §6.2 Control Data, §6.4 Content, §8.4 Content-Encoding, §9.3 Methods, §15.1 Status Codes, §15.3 Successful 2xx, §15.4 Redirects | -| **RFC 9111** | 8 | 8 | §2 Cache Overview, §3 Storing, §4.1 Vary/Keys, §4.2 Freshness, §4.3 Validation, §4.4 Invalidation, §5.1 Age, §5.2 Cache-Control | -| **RFC 9112** | 25 | 8 | §2 Message, §3 Request Line, §4 Status Line, §5 Field Syntax, §6 Message Body, §7 Transfer Codings, §8 Incomplete Messages, §9.3 Persistence | -| **RFC 9113** | 9 | 8 | §3.4 Preface, §4 Frames, §5 Streams, §6 Settings, §7 Error Codes, §8.1 Framing, §8.2 Fields, §9 Connections | -| **RFC 9114** | 10 | 8 | §4.1 Frames, §4.4 Streams, §6.2 Control Streams, §7.2.4 Settings, §8 Error Handling, §8.1 Framing, §10 Security, §A.2 Settings | - -**Last compliance doc update**: 2026-03-28 - -## Known Limitations & Gaps - -### Critical (Blocks Production Use) -1. ❌ **Server Implementation** — Only client-side encoders/decoders (No TurboServer yet) -2. 🔶 **Full QUIC Implementation** — Only primitives implemented; need full packet handling, handshake, migration - -### High Priority (Feature Gaps) -1. 🔶 **Connection Pooling Limits** — Per-host limits exist but not well-documented -2. 🔶 **Header DoS Protection** — No size/count limits (could OOM on large responses) -3. 🔶 **Max Concurrent Streams** — HTTP/2 client doesn't enforce server's MAX_CONCURRENT_STREAMS -4. 🔶 **Redirect Loop Detection** — Prevents infinite redirect chains (not enforced) -5. 🔶 **HTTPS→HTTP Protection** — Doesn't block cross-scheme downgrades - -### Medium Priority (RFC Edges) -1. 🟡 **Trailer Headers** — RFC 9112 §6.1 (rarely used) -2. 🟡 **Chunk Extensions** — RFC 9112 §6.1 (rarely used) -3. 🟡 **Public Suffix Cookies** — RFC 6265 public suffix list (limited third-party blocking) -4. 🟡 **Heuristic Freshness** — RFC 9111 heuristic caching (rarely needed) -5. 🟡 **Server Push** — HTTP/2 push promise acceptance (rarely used by clients) - -### Low Priority (Advanced Features) -1. 🟡 **Connection Migration** — QUIC connection migration (RFC 9000) -2. 🟡 **Datagram Extension** — RFC 9297 QUIC datagrams (future work) -3. 🟡 **Alt-Svc** — Alternative service advertisement (rarely used) -4. 🟡 **Proxy Support** — Proxy-Authorization, Proxy-Connection (enterprise use) - -## Path to Production - -### Phase 1: Stability (2 weeks) -- [ ] Add header size/count limits (RFC 9110 §5, RFC 9113 §6.5.2) -- [ ] Add redirect loop detection (prevent infinite chains) -- [ ] Add HTTPS→HTTP protection (RFC 9110 §15.4.6) -- [ ] Expand RFC9110 tests (2 → 10 test classes) - -### Phase 2: HTTP/3 (3-4 weeks) -- [ ] Complete HTTP/3 stream lifecycle -- [ ] Add HTTP/3 integration tests with Kestrel H3 -- [ ] Validate against spec with interop testing - -### Phase 3: Performance (2 weeks) -- [ ] Streaming request encoding (reduce allocation) -- [ ] SIMD CRLF detection (HTTP/1.1 faster) -- [ ] Benchmark-driven optimization - -### Phase 4: Features (2 weeks) -- [ ] Request/response logging (structured) -- [ ] Metrics/tracing (OpenTelemetry) -- [ ] Timeout policies (per-operation) - -### Phase 5: Release (1 week) -- [ ] NuGet packaging -- [ ] Version management (RELEASE_NOTES.md) -- [ ] Documentation site (VitePress) -- [ ] Example projects - -**Estimated Total**: 10-12 weeks to production v1.0 \ No newline at end of file diff --git a/notes/RFC/RFC1945/RFC1945.md b/notes/RFC/RFC1945/RFC1945.md index 616c6a67a..a6e07f633 100644 --- a/notes/RFC/RFC1945/RFC1945.md +++ b/notes/RFC/RFC1945/RFC1945.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 1945 — HTTP/1.0" rfc_number: 1945 description: "Hypertext Transfer Protocol version 1.0. Defines basic request/response message format, method semantics (GET, HEAD, POST), status codes, and simple entity body boundaries via Content-Length or connection close." @@ -11,17 +11,6 @@ aliases: [] **Official RFC**: [RFC 1945](https://www.rfc-editor.org/rfc/rfc1945) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 85/100 | -| **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC1945/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC1945/` — 17 files, 233 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC1945/` | -| **Key Gaps** | Streaming request encoding, header limit validation, connection reuse optimization | - ## Core Concepts Key ideas from this RFC, with links to section files: @@ -37,36 +26,6 @@ Key ideas from this RFC, with links to section files: - [[RFC1945/sections/20_10_4_content-length|Content-Length]] — Body framing via Content-Length header - [[RFC1945/sections/33_11_access_authentication|Access Authentication]] — Basic authentication scheme -## Implementation Notes - -### Encoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC1945/Http10Encoder.cs` | Serialise `HttpRequestMessage` to HTTP/1.0 wire format | - -### Decoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC1945/Http10DecoderPipeline.cs` | Stateful event-streaming decoder for HTTP/1.0 responses | -| `Protocol/RFC1945/Http10EventAggregator.cs` | Converts decoder event stream to `HttpResponseMessage` | -| `Protocol/RFC1945/Http10CompletionDecoder.cs` | Convenience wrapper: pipeline + aggregator | - -### Stages - -| File | Purpose | -|------|---------| -| `Streams/Stages/Encoding/Http10EncoderStage.cs` | Akka.Streams stage wrapping Http10Encoder | -| `Streams/Stages/Decoding/Http10DecoderStage.cs` | Akka.Streams stage wrapping Http10Decoder | - -### Tests - -| Location | Count | Focus | -|----------|-------|-------| -| `TurboHTTP.Tests/RFC1945/` | 233 tests | Protocol compliance | -| `TurboHTTP.StreamTests/RFC1945/` | — | Encoder/decoder/roundtrip stages, TCP fragmentation | - ## Sections | # | Section | File | Status | @@ -117,7 +76,6 @@ Key ideas from this RFC, with links to section files: ## See Also -- [[00-RFC_STATUS_MATRIX|RFC Status Matrix]] - [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps]] --- diff --git a/notes/RFC/RFC1945/sections/00_preamble.md b/notes/RFC/RFC1945/sections/00_preamble.md index 0c44adcf5..ae33154c4 100644 --- a/notes/RFC/RFC1945/sections/00_preamble.md +++ b/notes/RFC/RFC1945/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 1945 rfc_section: "preamble" @@ -9,12 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # Preamble - - - - - - Network Working Group T. Berners-Lee Request for Comments: 1945 MIT/LCS Category: Informational R. Fielding @@ -23,7 +17,6 @@ Category: Informational R. Fielding MIT/LCS May 1996 - Hypertext Transfer Protocol -- HTTP/1.0 Status of This Memo @@ -64,8 +57,6 @@ Table of Contents 2.2 Basic Rules .......................................... 10 3. Protocol Parameters ....................................... 12 - - ## 3.1 HTTP Version ......................................... 12 3.2 Uniform Resource Identifiers ......................... 14 3.2.1 General Syntax ................................ 14 @@ -115,8 +106,6 @@ Table of Contents 10.7 Expires ............................................. 41 10.8 From ................................................ 42 - - ## 10.9 If-Modified-Since ................................... 42 10.10 Last-Modified ....................................... 43 10.11 Location ............................................ 44 @@ -164,4 +153,3 @@ Table of Contents --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/02_1_introduction.md b/notes/RFC/RFC1945/sections/02_1_introduction.md index 6a4888104..1f2fbb507 100644 --- a/notes/RFC/RFC1945/sections/02_1_introduction.md +++ b/notes/RFC/RFC1945/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 1945 rfc_section: "1" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 1. Introduction - ## 1.1 Purpose The Hypertext Transfer Protocol (HTTP) is an application-level @@ -56,9 +55,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content sequence of octets matching the syntax defined in Section 4 and transmitted via the connection. - - - request An HTTP request message (as defined in Section 5). @@ -108,8 +104,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content possible translation, on to other servers. A proxy must interpret and, if necessary, rewrite a request message before - - forwarding it. Proxies are often used as client-side portals through network firewalls and as helper applications for handling requests via protocols not implemented by the user @@ -159,8 +153,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content followed by a MIME-like message containing request modifiers, client information, and possible body content. The server responds with a - - status line, including the message's protocol version and a success or error code, followed by a MIME-like message containing server information, entity metainformation, and possible body content. @@ -210,8 +202,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content participants along the chain has a cached response applicable to that request. The following illustrates the resulting chain if B has a - - cached copy of an earlier response from O (via C) for a request which has not been cached by UA or A. @@ -252,4 +242,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/03_2_notational_conventions_and_generic_grammar.md b/notes/RFC/RFC1945/sections/03_2_notational_conventions_and_generic_grammar.md index c60fe14d6..60a91eac0 100644 --- a/notes/RFC/RFC1945/sections/03_2_notational_conventions_and_generic_grammar.md +++ b/notes/RFC/RFC1945/sections/03_2_notational_conventions_and_generic_grammar.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Notational Conventions and Generic Grammar" rfc_number: 1945 rfc_section: "2" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 2. Notational Conventions and Generic Grammar - ## 2.1 Augmented BNF All of the mechanisms specified in this document are described in @@ -18,15 +17,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content notation in order to understand this specification. The augmented BNF includes the following constructs: - - - - ```abnf name = definition ``` - The name of a rule is simply the name itself (without any enclosing "<" and ">") and is separated from its definition by the equal character "=". Whitespace is only significant in that @@ -73,9 +67,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content (element). Thus 2DIGIT is a 2-digit number, and 3ALPHA is a string of three alphabetic characters. - - - #rule A construct "#" is defined, similar to "*", for defining lists @@ -120,7 +111,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content describe basic parsing constructs. The US-ASCII coded character set is defined by [17]. - ```abnf OCTET = CHAR = @@ -128,10 +118,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content LOALPHA = ``` - - - - ```abnf ALPHA = UPALPHA | LOALPHA DIGIT = @@ -144,28 +130,23 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content <"> = ``` - HTTP/1.0 defines the octet sequence CR LF as the end-of-line marker for all protocol elements except the Entity-Body (see Appendix B for tolerant applications). The end-of-line marker within an Entity-Body is defined by its associated media type, as described in Section 3.6. - ```abnf CRLF = CR LF ``` - HTTP/1.0 headers may be folded onto multiple lines if each continuation line begins with a space or horizontal tab. All linear whitespace, including folding, has the same semantics as SP. - ```abnf LWS = [CRLF] 1*( SP | HT ) ``` - However, folding of header lines is not expected by some applications, and should not be generated by HTTP/1.0 applications. @@ -173,40 +154,30 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content that are not intended to be interpreted by the message parser. Words of *TEXT may contain octets from character sets other than US-ASCII. - ```abnf TEXT = ``` - Recipients of header field TEXT containing octets outside the US- ASCII character set may assume that they represent ISO-8859-1 characters. Hexadecimal numeric characters are used in several protocol elements. - ```abnf HEX = "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f" | DIGIT ``` - Many HTTP/1.0 header field values consist of words separated by LWS or special characters. These special characters must be in a quoted string to be used within a parameter value. - ```abnf word = token | quoted-string ``` - - - - - ```abnf token = 1* @@ -216,24 +187,20 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content | "{" | "}" | SP | HT ``` - Comments may be included in some HTTP header fields by surrounding the comment text with parentheses. Comments are only allowed in fields containing "comment" as part of their field value definition. In all other fields, parentheses are considered part of the field value. - ```abnf comment = "(" *( ctext | comment ) ")" ctext = ``` - A string of text is parsed as a single word if it is quoted using double-quote marks. - ```abnf quoted-string = ( <"> *(qdtext) <"> ) @@ -241,10 +208,8 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content but including LWS> ``` - Single-character quoting using the backslash ("\") character is not permitted in HTTP/1.0. --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/04_3_1_http_version.md b/notes/RFC/RFC1945/sections/04_3_1_http_version.md index afdd73063..3071f4ec8 100644 --- a/notes/RFC/RFC1945/sections/04_3_1_http_version.md +++ b/notes/RFC/RFC1945/sections/04_3_1_http_version.md @@ -1,4 +1,4 @@ ---- +--- title: "3.1. HTTP Version" rfc_number: 1945 rfc_section: "3.1" @@ -9,8 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 3.1. HTTP Version - - ## 3.1 HTTP Version HTTP uses a "." numbering scheme to indicate versions @@ -31,16 +29,12 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content in the first line of the message. If the protocol version is not specified, the recipient must assume that the message is in the - - simple HTTP/0.9 format. - ```abnf HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT ``` - Note that the major and minor numbers should be treated as separate integers and that each may be incremented higher than a single digit. Thus, HTTP/2.4 is a lower version than HTTP/2.13, which in turn is @@ -84,4 +78,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/05_3_2_uniform_resource_identifiers.md b/notes/RFC/RFC1945/sections/05_3_2_uniform_resource_identifiers.md index e5964d024..01a98224a 100644 --- a/notes/RFC/RFC1945/sections/05_3_2_uniform_resource_identifiers.md +++ b/notes/RFC/RFC1945/sections/05_3_2_uniform_resource_identifiers.md @@ -1,4 +1,4 @@ ---- +--- title: "3.2. Uniform Resource Identifiers" rfc_number: 1945 rfc_section: "3.2" @@ -25,7 +25,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content forms are differentiated by the fact that absolute URIs always begin with a scheme name followed by a colon. - ```abnf URI = ( absoluteURI | relativeURI ) [ "#" fragment ] @@ -34,12 +33,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content relativeURI = net_path | abs_path | rel_path ``` - net_path = "//" net_loc [ abs_path ] abs_path = "/" rel_path rel_path = [ path ] [ ";" params ] [ "?" query ] - ```abnf path = fsegment *( "/" segment ) fsegment = 1*pchar @@ -65,9 +62,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content national = For definitive information on URL syntax and semantics, see RFC 1738 @@ -85,7 +79,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content http_URL = "http:" "//" host [ ":" port ] [ abs_path ] - ```abnf host = ``` - The order in which header fields are received is not significant. However, it is "good practice" to send General-Header fields first, followed by Request-Header or Response-Header fields prior to the @@ -103,9 +92,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content message, by appending each subsequent field-value to the first, each separated by a comma. - - - ## 4.3 General Header Fields There are a few header fields which have general applicability for @@ -113,13 +99,11 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content entity being transferred. These headers apply only to the message being transmitted. - ```abnf General-Header = Date ; Section 10.6 | Pragma ; Section 10.12 ``` - General header field names can be extended reliably only in combination with a change in the protocol version. However, new or experimental header fields may be given the semantics of general @@ -129,4 +113,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/12_5_request.md b/notes/RFC/RFC1945/sections/12_5_request.md index 716727ace..f2c997fc2 100644 --- a/notes/RFC/RFC1945/sections/12_5_request.md +++ b/notes/RFC/RFC1945/sections/12_5_request.md @@ -1,4 +1,4 @@ ---- +--- title: "5. Request" rfc_number: 1945 rfc_section: "5" @@ -9,15 +9,12 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 5. Request - - A request message from a client to a server includes, within the first line of that message, the method to be applied to the resource, the identifier of the resource, and the protocol version in use. For backwards compatibility with the more limited HTTP/0.9 protocol, there are two valid formats for an HTTP request: - ```abnf Request = Simple-Request | Full-Request @@ -31,7 +28,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content [ Entity-Body ] ; Section 7.2 ``` - If an HTTP/1.0 server receives a Simple-Request, it must respond with an HTTP/0.9 Simple-Response. An HTTP/1.0 client capable of receiving a Full-Response should never generate a Simple-Request. @@ -43,14 +39,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content elements are separated by SP characters. No CR or LF are allowed except in the final CRLF sequence. - ```abnf Request-Line = Method SP Request-URI SP HTTP-Version CRLF ``` - - - Note that the difference between a Simple-Request and the Request- Line of a Full-Request is the presence of the HTTP-Version field and the availability of methods other than GET. @@ -60,7 +52,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content The Method token indicates the method to be performed on the resource identified by the Request-URI. The method is case-sensitive. - ```abnf Method = "GET" ; Section 8.1 | "HEAD" ; Section 8.2 @@ -70,7 +61,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content extension-method = token ``` - The list of methods acceptable by a specific resource can change dynamically; the client is notified through the return code of the response if a method is not allowed on a resource. Servers should @@ -85,12 +75,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content The Request-URI is a Uniform Resource Identifier (Section 3.2) and identifies the resource upon which to apply the request. - ```abnf Request-URI = absoluteURI | abs_path ``` - The two options for Request-URI are dependent on the nature of the request. @@ -107,9 +95,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.0 - - - The most common form of Request-URI is that used to identify a resource on an origin server or gateway. In this case, only the absolute path of the URI is transmitted (see Section 3.2.1, @@ -136,7 +121,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content equivalent to the parameters on a programming language method (procedure) invocation. - ```abnf Request-Header = Authorization ; Section 10.2 | From ; Section 10.8 @@ -145,7 +129,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content | User-Agent ; Section 10.15 ``` - Request-Header field names can be extended reliably only in combination with a change in the protocol version. However, new or experimental header fields may be given the semantics of request @@ -155,4 +138,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/13_6_response.md b/notes/RFC/RFC1945/sections/13_6_response.md index 06c56ce7b..1a312e9fa 100644 --- a/notes/RFC/RFC1945/sections/13_6_response.md +++ b/notes/RFC/RFC1945/sections/13_6_response.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Response" rfc_number: 1945 rfc_section: "6" @@ -9,22 +9,15 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 6. Response - After receiving and interpreting a request message, a server responds in the form of an HTTP response message. - ```abnf Response = Simple-Response | Full-Response Simple-Response = [ Entity-Body ] ``` - - - - - ```abnf Full-Response = Status-Line ; Section 6.1 *( General-Header ; Section 4.3 @@ -34,7 +27,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content [ Entity-Body ] ; Section 7.2 ``` - A Simple-Response should only be sent in response to an HTTP/0.9 Simple-Request or if the server only supports the more limited HTTP/0.9 protocol. If a client sends an HTTP/1.0 Full-Request and @@ -50,12 +42,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content and its associated textual phrase, with each element separated by SP characters. No CR or LF is allowed except in the final CRLF sequence. - ```abnf Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF ``` - Since a status line always begins with the protocol version and status code @@ -78,11 +68,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content intended for the human user. The client is not required to examine or display the Reason-Phrase. - - - - - The first digit of the Status-Code defines the class of response. The last two digits do not have any categorization role. There are 5 values for the first digit: @@ -107,7 +92,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content -- they may be replaced by local equivalents without affecting the protocol. These codes are fully defined in Section 9. - ```abnf Status-Code = "200" ; OK | "201" ; Created @@ -131,13 +115,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content Reason-Phrase = * ``` - HTTP status codes are extensible, but the above codes are the only ones generally recognized in current practice. HTTP applications are not required to understand the meaning of all registered status - - codes, though such understanding is obviously desirable. However, applications must understand the class of any status code, as indicated by the first digit, and treat any unrecognized response as @@ -158,14 +139,12 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content Line. These header fields give information about the server and about further access to the resource identified by the Request-URI. - ```abnf Response-Header = Location ; Section 10.11 | Server ; Section 10.14 | WWW-Authenticate ; Section 10.16 ``` - Response-Header field names can be extended reliably only in combination with a change in the protocol version. However, new or experimental header fields may be given the semantics of response @@ -175,4 +154,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/14_7_entity.md b/notes/RFC/RFC1945/sections/14_7_entity.md index 25817a35c..e00cd520a 100644 --- a/notes/RFC/RFC1945/sections/14_7_entity.md +++ b/notes/RFC/RFC1945/sections/14_7_entity.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Entity" rfc_number: 1945 rfc_section: "7" @@ -9,32 +9,18 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 7. Entity - Full-Request and Full-Response messages may transfer an entity within some requests and responses. An entity consists of Entity-Header fields and (usually) an Entity-Body. In this section, both sender and recipient refer to either the client or the server, depending on who sends and who receives the entity. - - - - - - - - - - - - ## 7.1 Entity Header Fields Entity-Header fields define optional metainformation about the Entity-Body or, if no body is present, about the resource identified by the request. - ```abnf Entity-Header = Allow ; Section 10.1 | Content-Encoding ; Section 10.3 @@ -47,7 +33,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content extension-header = HTTP-header ``` - The extension-header mechanism allows additional Entity-Header fields to be defined without changing the protocol, but these fields cannot be assumed to be recognizable by the recipient. Unrecognized header @@ -58,12 +43,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content The entity body (if any) sent with an HTTP request or response is in a format and encoding defined by the Entity-Header fields. - ```abnf Entity-Body = *OCTET ``` - An entity body is included with a request message only when the request method calls for one. The presence of an entity body in a request is signaled by the inclusion of a Content-Length header field @@ -85,8 +68,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content body is determined via the header fields Content-Type and Content- Encoding. These define a two-layer, ordered encoding model: - - entity-body := Content-Encoding( Content-Type( data ) ) A Content-Type specifies the media type of the underlying data. A @@ -131,4 +112,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/15_8_method_definitions.md b/notes/RFC/RFC1945/sections/15_8_method_definitions.md index db1594568..13bf408d9 100644 --- a/notes/RFC/RFC1945/sections/15_8_method_definitions.md +++ b/notes/RFC/RFC1945/sections/15_8_method_definitions.md @@ -1,4 +1,4 @@ ---- +--- title: "8. Method Definitions" rfc_number: 1945 rfc_section: "8" @@ -9,14 +9,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 8. Method Definitions - The set of common methods for HTTP/1.0 is defined below. Although this set can be expanded, additional methods cannot be assumed to share the same semantics for separately extended clients and servers. - - - ## 8.1 GET The GET method means retrieve whatever information (in the form of an @@ -66,8 +62,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content o Extending a database through an append operation. - - The actual function performed by the POST method is determined by the server and is usually dependent on the Request-URI. The posted entity is subordinate to that URI in the same way that a file is subordinate @@ -98,4 +92,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/16_9_status_code_definitions.md b/notes/RFC/RFC1945/sections/16_9_status_code_definitions.md index 4ec9e687c..f8f15a917 100644 --- a/notes/RFC/RFC1945/sections/16_9_status_code_definitions.md +++ b/notes/RFC/RFC1945/sections/16_9_status_code_definitions.md @@ -1,4 +1,4 @@ ---- +--- title: "9. Status Code Definitions" rfc_number: 1945 rfc_section: "9" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 9. Status Code Definitions - Each Status-Code is described below, including a description of which method(s) it can follow and any metainformation required in the response. @@ -28,9 +27,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content This class of status code indicates that the client's request was successfully received, understood, and accepted. - - - 200 OK The request has succeeded. The information returned with the @@ -80,8 +76,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content information to send back. If the client is a user agent, it should not change its document view from that which caused the request to - - be generated. This response is primarily intended to allow input for scripts or other actions to take place without causing a change to the user agent's active document view. The response may include @@ -129,10 +123,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content request unless it can be confirmed by the user, since this might change the conditions under which the request was issued. - - - - Note: When automatically redirecting a POST request after receiving a 301 status code, some existing user agents will erroneously change it into a GET request. @@ -179,11 +169,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content error situation, and whether it is a temporary or permanent condition. These status codes are applicable to any request method. - - - - - Note: If the client is sending data, server implementations on TCP should be careful to ensure that the client acknowledges receipt of the packet(s) containing the response prior to closing the @@ -233,8 +218,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content available to the client, the status code 403 (forbidden) can be used instead. - - ## 9.5 Server Error 5xx Response status codes beginning with the digit "5" indicate cases in @@ -278,4 +261,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/17_10_1_allow.md b/notes/RFC/RFC1945/sections/17_10_1_allow.md index 01de1c221..77d93fba9 100644 --- a/notes/RFC/RFC1945/sections/17_10_1_allow.md +++ b/notes/RFC/RFC1945/sections/17_10_1_allow.md @@ -1,4 +1,4 @@ ---- +--- title: "10.1. Allow" rfc_number: 1945 rfc_section: "10.1" @@ -9,16 +9,11 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 10.1. Allow - - This section defines the syntax and semantics of all commonly used HTTP/1.0 header fields. For general and entity header fields, both sender and recipient refer to either the client or the server, depending on who sends and who receives the message. - - - ## 10.1 Allow The Allow entity-header field lists the set of methods supported by @@ -28,12 +23,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content using the POST method, and thus should be ignored if it is received as part of a POST entity. - ```abnf Allow = "Allow" ":" 1#method ``` - Example of use: Allow: GET, HEAD @@ -52,4 +45,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/18_10_2_authorization.md b/notes/RFC/RFC1945/sections/18_10_2_authorization.md index 78d597bb6..2c4fc3d6a 100644 --- a/notes/RFC/RFC1945/sections/18_10_2_authorization.md +++ b/notes/RFC/RFC1945/sections/18_10_2_authorization.md @@ -1,4 +1,4 @@ ---- +--- title: "10.2. Authorization" rfc_number: 1945 rfc_section: "10.2" @@ -18,12 +18,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content containing the authentication information of the user agent for the realm of the resource being requested. - ```abnf Authorization = "Authorization" ":" credentials ``` - HTTP access authentication is described in Section 11. If a request is authenticated and a realm specified, the same credentials should be valid for all other requests within this realm. @@ -33,4 +31,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/19_10_3_content-encoding.md b/notes/RFC/RFC1945/sections/19_10_3_content-encoding.md index 65e0564dc..531a0c977 100644 --- a/notes/RFC/RFC1945/sections/19_10_3_content-encoding.md +++ b/notes/RFC/RFC1945/sections/19_10_3_content-encoding.md @@ -1,4 +1,4 @@ ---- +--- title: "10.3. Content-Encoding" rfc_number: 1945 rfc_section: "10.3" @@ -19,12 +19,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content primarily used to allow a document to be compressed without losing the identity of its underlying media type. - ```abnf Content-Encoding = "Content-Encoding" ":" content-coding ``` - Content codings are defined in Section 3.5. An example of its use is Content-Encoding: x-gzip @@ -35,4 +33,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/20_10_4_content-length.md b/notes/RFC/RFC1945/sections/20_10_4_content-length.md index 9ac4aa730..b50bd53e9 100644 --- a/notes/RFC/RFC1945/sections/20_10_4_content-length.md +++ b/notes/RFC/RFC1945/sections/20_10_4_content-length.md @@ -1,4 +1,4 @@ ---- +--- title: "10.4. Content-Length" rfc_number: 1945 rfc_section: "10.4" @@ -16,12 +16,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content in the case of the HEAD method, the size of the Entity-Body that would have been sent had the request been a GET. - ```abnf Content-Length = "Content-Length" ":" 1*DIGIT ``` - An example is Content-Length: 3495 @@ -43,4 +41,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/21_10_5_content-type.md b/notes/RFC/RFC1945/sections/21_10_5_content-type.md index b8c01b749..7736627e3 100644 --- a/notes/RFC/RFC1945/sections/21_10_5_content-type.md +++ b/notes/RFC/RFC1945/sections/21_10_5_content-type.md @@ -1,4 +1,4 @@ ---- +--- title: "10.5. Content-Type" rfc_number: 1945 rfc_section: "10.5" @@ -15,12 +15,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content Entity-Body sent to the recipient or, in the case of the HEAD method, the media type that would have been sent had the request been a GET. - ```abnf Content-Type = "Content-Type" ":" media-type ``` - Media types are defined in Section 3.6. An example of the field is Content-Type: text/html @@ -30,4 +28,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/22_10_6_date.md b/notes/RFC/RFC1945/sections/22_10_6_date.md index f91c40e17..d7797ee93 100644 --- a/notes/RFC/RFC1945/sections/22_10_6_date.md +++ b/notes/RFC/RFC1945/sections/22_10_6_date.md @@ -1,4 +1,4 @@ ---- +--- title: "10.6. Date" rfc_number: 1945 rfc_section: "10.6" @@ -16,12 +16,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content RFC 822. The field value is an HTTP-date, as described in Section 3.3. - ```abnf Date = "Date" ":" HTTP-date ``` - An example is Date: Tue, 15 Nov 1994 08:12:31 GMT @@ -47,10 +45,7 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content that this field should contain the creation date of the enclosed Entity-Body. This has been changed to reflect actual (and proper) - - usage. --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/23_10_7_expires.md b/notes/RFC/RFC1945/sections/23_10_7_expires.md index 93cfcdf4e..4fa1fe1df 100644 --- a/notes/RFC/RFC1945/sections/23_10_7_expires.md +++ b/notes/RFC/RFC1945/sections/23_10_7_expires.md @@ -1,4 +1,4 @@ ---- +--- title: "10.7. Expires" rfc_number: 1945 rfc_section: "10.7" @@ -22,12 +22,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content should include an Expires header with that date. The format is an absolute date and time as defined by HTTP-date in Section 3.3. - ```abnf Expires = "Expires" ":" HTTP-date ``` - An example of its use is Expires: Thu, 01 Dec 1994 16:00:00 GMT @@ -59,4 +57,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/24_10_8_from.md b/notes/RFC/RFC1945/sections/24_10_8_from.md index a1d7a59e3..4f1832125 100644 --- a/notes/RFC/RFC1945/sections/24_10_8_from.md +++ b/notes/RFC/RFC1945/sections/24_10_8_from.md @@ -1,4 +1,4 @@ ---- +--- title: "10.8. From" rfc_number: 1945 rfc_section: "10.8" @@ -16,12 +16,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content agent. The address should be machine-usable, as defined by mailbox in RFC 822 [7] (as updated by RFC 1123 [6]): - ```abnf From = "From" ":" mailbox ``` - An example is: From: webmaster@w3.org @@ -48,4 +46,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/25_10_9_if-modified-since.md b/notes/RFC/RFC1945/sections/25_10_9_if-modified-since.md index 86c4cd00d..419afb080 100644 --- a/notes/RFC/RFC1945/sections/25_10_9_if-modified-since.md +++ b/notes/RFC/RFC1945/sections/25_10_9_if-modified-since.md @@ -1,4 +1,4 @@ ---- +--- title: "10.9. If-Modified-Since" rfc_number: 1945 rfc_section: "10.9" @@ -17,20 +17,14 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content resource will not be returned from the server; instead, a 304 (not modified) response will be returned without any Entity-Body. - ```abnf If-Modified-Since = "If-Modified-Since" ":" HTTP-date ``` - An example of the field is: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT - - - - A conditional GET method requests that the identified resource be transferred only if it has been modified since the date given by the If-Modified-Since header. The algorithm for determining this includes @@ -55,4 +49,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/26_10_10_last-modified.md b/notes/RFC/RFC1945/sections/26_10_10_last-modified.md index 7cdd4e91a..4061c1a3a 100644 --- a/notes/RFC/RFC1945/sections/26_10_10_last-modified.md +++ b/notes/RFC/RFC1945/sections/26_10_10_last-modified.md @@ -1,4 +1,4 @@ ---- +--- title: "10.10. Last-Modified" rfc_number: 1945 rfc_section: "10.10" @@ -18,12 +18,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content which is older than the date given by the Last-Modified field, that copy should be considered stale. - ```abnf Last-Modified = "Last-Modified" ":" HTTP-date ``` - An example of its use is Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT @@ -40,11 +38,8 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content than the server's time of message origination. In such cases, where the resource's last modification would indicate some time in the - - future, the server must replace that date with the message origination date. --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/27_10_11_location.md b/notes/RFC/RFC1945/sections/27_10_11_location.md index 10a544592..a2a94b009 100644 --- a/notes/RFC/RFC1945/sections/27_10_11_location.md +++ b/notes/RFC/RFC1945/sections/27_10_11_location.md @@ -1,4 +1,4 @@ ---- +--- title: "10.11. Location" rfc_number: 1945 rfc_section: "10.11" @@ -16,16 +16,13 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content the location must indicate the server's preferred URL for automatic redirection to the resource. Only one absolute URL is allowed. - ```abnf Location = "Location" ":" absoluteURI ``` - An example is Location: http://www.w3.org/hypertext/WWW/NewLocation.html --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/28_10_12_pragma.md b/notes/RFC/RFC1945/sections/28_10_12_pragma.md index 9017a7b8c..efefb30aa 100644 --- a/notes/RFC/RFC1945/sections/28_10_12_pragma.md +++ b/notes/RFC/RFC1945/sections/28_10_12_pragma.md @@ -1,4 +1,4 @@ ---- +--- title: "10.12. Pragma" rfc_number: 1945 rfc_section: "10.12" @@ -17,7 +17,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content behavior from the viewpoint of the protocol; however, some systems may require that behavior be consistent with the directives. - ```abnf Pragma = "Pragma" ":" 1#pragma-directive @@ -25,7 +24,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content extension-pragma = token [ "=" word ] ``` - When the "no-cache" directive is present in a request message, an application should forward the request toward the origin server even if it has a cached copy of what is being requested. This allows a @@ -42,4 +40,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/29_10_13_referer.md b/notes/RFC/RFC1945/sections/29_10_13_referer.md index 0aa61efb5..570e085c1 100644 --- a/notes/RFC/RFC1945/sections/29_10_13_referer.md +++ b/notes/RFC/RFC1945/sections/29_10_13_referer.md @@ -1,4 +1,4 @@ ---- +--- title: "10.13. Referer" rfc_number: 1945 rfc_section: "10.13" @@ -15,20 +15,16 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content the server's benefit, the address (URI) of the resource from which the Request-URI was obtained. This allows a server to generate lists - - of back-links to resources for interest, logging, optimized caching, etc. It also allows obsolete or mistyped links to be traced for maintenance. The Referer field must not be sent if the Request-URI was obtained from a source that does not have its own URI, such as input from the user keyboard. - ```abnf Referer = "Referer" ":" ( absoluteURI | relativeURI ) ``` - Example: Referer: http://www.w3.org/hypertext/DataSources/Overview.html @@ -46,4 +42,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/30_10_14_server.md b/notes/RFC/RFC1945/sections/30_10_14_server.md index 90b98ae76..f2224fe0c 100644 --- a/notes/RFC/RFC1945/sections/30_10_14_server.md +++ b/notes/RFC/RFC1945/sections/30_10_14_server.md @@ -1,4 +1,4 @@ ---- +--- title: "10.14. Server" rfc_number: 1945 rfc_section: "10.14" @@ -18,12 +18,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content convention, the product tokens are listed in order of their significance for identifying the application. - ```abnf Server = "Server" ":" 1*( product | comment ) ``` - Example: Server: CERN/3.0 libwww/2.17 @@ -37,13 +35,8 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content implementors are encouraged to make this field a configurable option. - - - - Note: Some existing servers fail to restrict themselves to the product token syntax within the Server field. --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/31_10_15_user-agent.md b/notes/RFC/RFC1945/sections/31_10_15_user-agent.md index e2f0b4364..23457d65d 100644 --- a/notes/RFC/RFC1945/sections/31_10_15_user-agent.md +++ b/notes/RFC/RFC1945/sections/31_10_15_user-agent.md @@ -1,4 +1,4 @@ ---- +--- title: "10.15. User-Agent" rfc_number: 1945 rfc_section: "10.15" @@ -22,12 +22,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content convention, the product tokens are listed in order of their significance for identifying the application. - ```abnf User-Agent = "User-Agent" ":" 1*( product | comment ) ``` - Example: User-Agent: CERN-LineMode/2.15 libwww/2.17b3 @@ -42,4 +40,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/32_10_16_www-authenticate.md b/notes/RFC/RFC1945/sections/32_10_16_www-authenticate.md index 1981d4d11..e27f170f8 100644 --- a/notes/RFC/RFC1945/sections/32_10_16_www-authenticate.md +++ b/notes/RFC/RFC1945/sections/32_10_16_www-authenticate.md @@ -1,4 +1,4 @@ ---- +--- title: "10.16. WWW-Authenticate" rfc_number: 1945 rfc_section: "10.16" @@ -16,12 +16,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content least one challenge that indicates the authentication scheme(s) and parameters applicable to the Request-URI. - ```abnf WWW-Authenticate = "WWW-Authenticate" ":" 1#challenge ``` - The HTTP access authentication process is described in Section 11. User agents must take special care in parsing the WWW-Authenticate field value if it contains more than one challenge, or if more than @@ -31,4 +29,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/33_11_access_authentication.md b/notes/RFC/RFC1945/sections/33_11_access_authentication.md index a346811e2..eed9d021b 100644 --- a/notes/RFC/RFC1945/sections/33_11_access_authentication.md +++ b/notes/RFC/RFC1945/sections/33_11_access_authentication.md @@ -1,4 +1,4 @@ ---- +--- title: "11. Access Authentication" rfc_number: 1945 rfc_section: "11" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 11. Access Authentication - HTTP provides a simple challenge-response authentication mechanism which may be used by a server to challenge a client request and by a client to provide authentication information. It uses an extensible, @@ -18,20 +17,17 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content carry the parameters necessary for achieving authentication via that scheme. - ```abnf auth-scheme = token auth-param = token "=" quoted-string ``` - The 401 (unauthorized) response message is used by an origin server to challenge the authorization of a user agent. This response must include a WWW-Authenticate header field containing at least one challenge applicable to the requested resource. - ```abnf challenge = auth-scheme 1*SP realm *( "," auth-param ) @@ -39,7 +35,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content realm-value = quoted-string ``` - The realm attribute (case-insensitive) is required for all authentication schemes which issue a challenge. The realm value (case-sensitive), in combination with the canonical root URL of the @@ -57,20 +52,16 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content authentication information of the user agent for the realm of the resource being requested. - ```abnf credentials = basic-credentials | ( auth-scheme #auth-param ) ``` - The domain over which credentials can be automatically applied by a user agent is determined by the protection space. If a prior request has been authorized, the same credentials may be reused for all other requests within that protection space for a period of time determined - - by the authentication scheme, parameters, and/or user preference. Unless otherwise defined by the authentication scheme, a single protection space cannot extend outside the scope of its server. @@ -114,7 +105,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content separated by a single colon (":") character, within a base64 [5] encoded string in the credentials. - ```abnf basic-credentials = "Basic" SP basic-cookie @@ -122,16 +112,10 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content except not limited to 76 char/line> ``` - - - - - ```abnf userid-password = [ token ] ":" *TEXT ``` - If the user agent wishes to send the user-ID "Aladdin" and password "open sesame", it would use the following header field: @@ -147,4 +131,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/34_12_security_considerations.md b/notes/RFC/RFC1945/sections/34_12_security_considerations.md index 48f8ece89..c76d02d8b 100644 --- a/notes/RFC/RFC1945/sections/34_12_security_considerations.md +++ b/notes/RFC/RFC1945/sections/34_12_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "12. Security Considerations" rfc_number: 1945 rfc_section: "12" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 12. Security Considerations - This section is meant to inform application developers, information providers, and users of the security limitations in HTTP/1.0 as described by this document. The discussion does not include @@ -40,10 +39,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content special way, so that the user is made aware of the fact that a possibly unsafe action is being requested. - - - - Naturally, it is not possible to ensure that the server does not generate side-effects as a result of performing a GET request; in fact, some dynamic resources consider that a feature. The important @@ -93,8 +88,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content be provided for the user to enable or disable the sending of From and Referer information. - - ## 12.5 Attacks Based On File and Path Names Implementations of HTTP origin servers should be careful to restrict @@ -116,4 +109,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/35_13_acknowledgments.md b/notes/RFC/RFC1945/sections/35_13_acknowledgments.md index ecef56fd2..87307f934 100644 --- a/notes/RFC/RFC1945/sections/35_13_acknowledgments.md +++ b/notes/RFC/RFC1945/sections/35_13_acknowledgments.md @@ -1,4 +1,4 @@ ---- +--- title: "13. Acknowledgments" rfc_number: 1945 rfc_section: "13" @@ -9,7 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 13. Acknowledgments - This specification makes heavy use of the augmented BNF and generic constructs defined by David H. Crocker for RFC 822 [7]. Similarly, it reuses many of the definitions provided by Nathaniel Borenstein and @@ -31,15 +30,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content Paul Hoffman contributed sections regarding the informational status of this document and Appendices C and D. - - - - - - - - - This document has benefited greatly from the comments of all those participating in the HTTP-WG. In addition to those already mentioned, the following individuals have contributed to this specification: @@ -68,4 +58,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC1945/sections/86_14_references.md b/notes/RFC/RFC1945/sections/86_14_references.md index 7ad2c6013..39033ae27 100644 --- a/notes/RFC/RFC1945/sections/86_14_references.md +++ b/notes/RFC/RFC1945/sections/86_14_references.md @@ -1,4 +1,4 @@ ---- +--- title: "14. References" rfc_number: 1945 rfc_section: "14" @@ -9,8 +9,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content # 14. References - - [1] Anklesaria, F., McCahill, M., Lindner, P., Johnson, D., Torrey, D., and B. Alberti, "The Internet Gopher Protocol: A Distributed Document Search and Retrieval Protocol", RFC 1436, @@ -28,12 +26,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content Resource Locators (URL)", RFC 1738, CERN, Xerox PARC, University of Minnesota, December 1994. - - - - - - [5] Borenstein, N., and N. Freed, "MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing the Format of Internet Message Bodies", RFC 1521, Bellcore, @@ -81,10 +73,6 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content for Information Interchange. Standard ANSI X3.4-1986, ANSI, 1986. - - - - [18] ISO-8859. International Standard -- Information Processing -- 8-bit Single-Byte Coded Graphic Character Sets -- Part 1: Latin alphabet No. 1, ISO 8859-1:1987. @@ -99,4 +87,3 @@ tags: [RFC1945, HTTP/1.0, message-syntax, request-response, entity-body, content --- -**Navigation:** [[../RFC1945|RFC1945 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/RFC6265.md b/notes/RFC/RFC6265/RFC6265.md index 6188eafd2..cde031c50 100644 --- a/notes/RFC/RFC6265/RFC6265.md +++ b/notes/RFC/RFC6265/RFC6265.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 6265 — HTTP State Management (Cookies)" rfc_number: 6265 description: "HTTP cookie mechanism for state management. Defines Set-Cookie/Cookie headers, domain and path matching, cookie attributes (Secure, HttpOnly, SameSite, Max-Age, Expires), and storage model." @@ -11,17 +11,6 @@ aliases: [] **Official RFC**: [RFC 6265](https://www.rfc-editor.org/rfc/rfc6265) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 80/100 | -| **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC6265/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC6265/` — 2 files, 66 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC6265/` | -| **Key Gaps** | Public suffix list, third-party cookie blocking, IP address handling, origin validation | - ## Core Concepts - [[RFC6265/sections/02_1_introduction|Introduction]] — Overview of cookie mechanism @@ -32,27 +21,6 @@ aliases: [] - [[RFC6265/sections/16_12_insert_the_newly_created_cookie_into_the_cookie_st|Cookie Storage]] — Cookie jar insertion algorithm - [[RFC6265/sections/21_8_security_considerations|Security Considerations]] — Cookie security issues and mitigations -## Implementation Notes - -### Encoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC6265/CookieJar.cs` | Cookie storage, domain/path matching, injection | - -### Stages - -| File | Purpose | -|------|---------| -| `Streams/Stages/Features/CookieBidiStage.cs` | Cookie injection and storage BidiStage | - -### Tests - -| Location | Count | Focus | -|----------|-------|-------| -| `TurboHTTP.Tests/RFC6265/` | 66 tests | Cookie parsing, matching, attributes | -| `TurboHTTP.StreamTests/RFC6265/` | — | Cookie injection and storage stage tests | - ## Sections | # | Section | File | Status | @@ -91,7 +59,6 @@ aliases: [] ## See Also -- [[00-RFC_STATUS_MATRIX|RFC Status Matrix]] - [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps]] --- diff --git a/notes/RFC/RFC6265/sections/00_preamble.md b/notes/RFC/RFC6265/sections/00_preamble.md index cdef95014..1259d1c8f 100644 --- a/notes/RFC/RFC6265/sections/00_preamble.md +++ b/notes/RFC/RFC6265/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 6265 rfc_section: "preamble" @@ -9,19 +9,12 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # Preamble - - - - - - Internet Engineering Task Force (IETF) A. Barth Request for Comments: 6265 U.C. Berkeley Obsoletes: 2965 April 2011 Category: Standards Track ISSN: 2070-1721 - HTTP State Management Mechanism Abstract @@ -63,9 +56,6 @@ Copyright Notice the Trust Legal Provisions and are provided without warranty as described in the Simplified BSD License. - - - This document may contain material from IETF Documents or IETF Contributions published or made publicly available before November 10, 2008. The person(s) controlling the copyright in some of this @@ -117,4 +107,3 @@ Table of Contents --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/02_1_introduction.md b/notes/RFC/RFC6265/sections/02_1_introduction.md index e0d9d07c3..c18045a52 100644 --- a/notes/RFC/RFC6265/sections/02_1_introduction.md +++ b/notes/RFC/RFC6265/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 6265 rfc_section: "1" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 1. Introduction - This document defines the HTTP Cookie and Set-Cookie header fields. Using the Set-Cookie header field, an HTTP server can pass name/value pairs and associated metadata (called cookies) to a user agent. When @@ -36,8 +35,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat There are two audiences for this specification: developers of cookie- generating servers and developers of cookie-consuming user agents. - - > **SHOULD**: To maximize interoperability with user agents, servers SHOULD limit themselves to the well-behaved profile defined in Section 4 when generating cookies. @@ -71,4 +68,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/03_2_change_the_status_of_rfc2965_to_historic.md b/notes/RFC/RFC6265/sections/03_2_change_the_status_of_rfc2965_to_historic.md index e732729c5..2d8d66370 100644 --- a/notes/RFC/RFC6265/sections/03_2_change_the_status_of_rfc2965_to_historic.md +++ b/notes/RFC/RFC6265/sections/03_2_change_the_status_of_rfc2965_to_historic.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Change the status of [RFC2965] to Historic." rfc_number: 6265 rfc_section: "2" @@ -9,7 +9,5 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 2. Change the status of [RFC2965] to Historic. - --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/04_3_indicate_that_rfc2965_has_been_obsoleted_by_this_d.md b/notes/RFC/RFC6265/sections/04_3_indicate_that_rfc2965_has_been_obsoleted_by_this_d.md index 1f5e673ab..f37952e6a 100644 --- a/notes/RFC/RFC6265/sections/04_3_indicate_that_rfc2965_has_been_obsoleted_by_this_d.md +++ b/notes/RFC/RFC6265/sections/04_3_indicate_that_rfc2965_has_been_obsoleted_by_this_d.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Indicate that [RFC2965] has been obsoleted by this document." rfc_number: 6265 rfc_section: "3" @@ -9,11 +9,9 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 3. Indicate that [RFC2965] has been obsoleted by this document. - In particular, in moving RFC 2965 to Historic and obsoleting it, this document deprecates the use of the Cookie2 and Set-Cookie2 header fields. --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/05_2_conventions.md b/notes/RFC/RFC6265/sections/05_2_conventions.md index 32a2e59e7..9b3605121 100644 --- a/notes/RFC/RFC6265/sections/05_2_conventions.md +++ b/notes/RFC/RFC6265/sections/05_2_conventions.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Conventions" rfc_number: 6265 rfc_section: "2" @@ -9,17 +9,12 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 2. Conventions - ## 2.1. Conformance Criteria > **MUST**: The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119]. - - - - > **MUST**: Requirements phrased in the imperative as part of algorithms (such as "strip any leading space characters" or "return false and abort these steps") are to be interpreted with the meaning of the key word @@ -47,14 +42,12 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat The OWS (optional whitespace) rule is used where zero or more linear > **MAY**: whitespace characters MAY appear: - ```abnf OWS = *( [ obs-fold ] WSP ) ; "optional" whitespace obs-fold = CRLF ``` - > **SHOULD**: OWS SHOULD either not be produced or be produced as a single SP character. @@ -71,10 +64,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat The term request-uri is defined in Section 5.1.2 of [RFC2616]. - - - - Two sequences of octets are said to case-insensitively match each other if and only if they are equivalent under the i;ascii-casemap collation defined in [RFC4790]. @@ -83,4 +72,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/06_3_overview.md b/notes/RFC/RFC6265/sections/06_3_overview.md index 22676b063..ac85b0fc4 100644 --- a/notes/RFC/RFC6265/sections/06_3_overview.md +++ b/notes/RFC/RFC6265/sections/06_3_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Overview" rfc_number: 6265 rfc_section: "3" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 3. Overview - This section outlines a way for an origin server to send state information to a user agent and for the user agent to return the state information to the origin server. @@ -45,14 +44,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat with the value 31d4d96e407aad42. The user agent then returns the session identifier in subsequent requests. - - - - - - - - == Server -> User Agent == Set-Cookie: SID=31d4d96e407aad42 @@ -98,12 +89,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat the expiration date if the user agent's cookie store exceeds its quota or if the user manually deletes the server's cookie. - - - - - - == Server -> User Agent == Set-Cookie: lang=en-US; Expires=Wed, 09 Jun 2021 10:18:14 GMT @@ -128,4 +113,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/07_4_server_requirements.md b/notes/RFC/RFC6265/sections/07_4_server_requirements.md index 7883fad40..2d1f67187 100644 --- a/notes/RFC/RFC6265/sections/07_4_server_requirements.md +++ b/notes/RFC/RFC6265/sections/07_4_server_requirements.md @@ -1,4 +1,4 @@ ---- +--- title: "4. Server Requirements" rfc_number: 6265 rfc_section: "4" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 4. Server Requirements - This section describes the syntax and semantics of a well-behaved profile of the Cookie and Set-Cookie headers. @@ -26,17 +25,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat > **SHOULD NOT**: Servers SHOULD NOT send Set-Cookie headers that fail to conform to the following grammar: - - - - - - - - - - - set-cookie-header = "Set-Cookie:" SP set-cookie-string set-cookie-string = cookie-pair *( ";" SP cookie-av ) cookie-pair = cookie-name "=" cookie-value @@ -85,9 +73,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat same set-cookie-string. (See Section 5.3 for how user agents handle this case.) - - - > **SHOULD NOT**: Servers SHOULD NOT include more than one Set-Cookie header field in the same response with the same cookie-name. (See Section 5.2 for how user agents handle this case.) @@ -130,15 +115,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat defined by the user agent). User agents ignore unrecognized cookie attributes (but not the entire cookie). - - - - - - - - - ### 4.1.2.1. The Expires Attribute The Expires attribute indicates the maximum lifetime of the cookie, @@ -183,13 +159,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat Cookie header without a Domain attribute, these user agents will erroneously send the cookie to www.example.com as well. - - - - - - - The user agent will reject cookies unless the Domain attribute specifies a scope for the cookie that would include the origin server. For example, the user agent will accept a cookie with a @@ -233,14 +202,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat cookies from an insecure channel, disrupting their integrity (see Section 8.6 for more details). - - - - - - - - ### 4.1.2.6. The HttpOnly Attribute The HttpOnly attribute limits the scope of the cookie to HTTP @@ -262,13 +223,11 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat Section 5), the user agent will send a Cookie header that conforms to the following grammar: - ```abnf cookie-header = "Cookie:" OWS cookie-string OWS cookie-string = cookie-pair *( ";" SP cookie-pair ) ``` - ### 4.2.2. Semantics Each cookie-pair represents a cookie stored by the user agent. The @@ -294,4 +253,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/08_5_user_agent_requirements.md b/notes/RFC/RFC6265/sections/08_5_user_agent_requirements.md index 4044d5aa2..ef8005ebc 100644 --- a/notes/RFC/RFC6265/sections/08_5_user_agent_requirements.md +++ b/notes/RFC/RFC6265/sections/08_5_user_agent_requirements.md @@ -1,4 +1,4 @@ ---- +--- title: 5. User Agent Requirements rfc_number: 6265 rfc_section: "5" @@ -18,8 +18,6 @@ tags: # 5. User Agent Requirements - - This section specifies the Cookie and Set-Cookie headers in sufficient detail that a user agent implementing these requirements precisely can interoperate with existing servers (even those that do @@ -44,4 +42,3 @@ tags: --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/09_1_using_the_grammar_below_divide_the_cookie-date_int.md b/notes/RFC/RFC6265/sections/09_1_using_the_grammar_below_divide_the_cookie-date_int.md index 9ec505f85..8f2b6a4f1 100644 --- a/notes/RFC/RFC6265/sections/09_1_using_the_grammar_below_divide_the_cookie-date_int.md +++ b/notes/RFC/RFC6265/sections/09_1_using_the_grammar_below_divide_the_cookie-date_int.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Using the grammar below, divide the cookie-date into date-tokens." rfc_number: 6265 rfc_section: "1" @@ -9,8 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 1. Using the grammar below, divide the cookie-date into date-tokens. - - ```abnf cookie-date = *delimiter date-token-list *delimiter date-token-list = date-token *( 1*delimiter date-token ) @@ -30,15 +28,9 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat time-field = 1*2DIGIT ``` - 2. Process each date-token sequentially in the order the date-tokens appear in the cookie-date: - - - - - 1. If the found-time flag is not set and the token matches the time production, set the found-time flag and set the hour- value, minute-value, and second-value to the numbers denoted @@ -72,4 +64,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/10_5_abort_these_steps_and_fail_to_parse_the_cookie-dat.md b/notes/RFC/RFC6265/sections/10_5_abort_these_steps_and_fail_to_parse_the_cookie-dat.md index 4796525ac..e61385354 100644 --- a/notes/RFC/RFC6265/sections/10_5_abort_these_steps_and_fail_to_parse_the_cookie-dat.md +++ b/notes/RFC/RFC6265/sections/10_5_abort_these_steps_and_fail_to_parse_the_cookie-dat.md @@ -1,4 +1,4 @@ ---- +--- title: "5. Abort these steps and fail to parse the cookie-date if:" rfc_number: 6265 rfc_section: "5" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 5. Abort these steps and fail to parse the cookie-date if: - * at least one of the found-day-of-month, found-month, found- year, or found-time flags is not set, @@ -25,9 +24,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat (Note that leap seconds cannot be represented in this syntax.) - - - 6. Let the parsed-cookie-date be the date whose day-of-month, month, year, hour, minute, and second (in UTC) are the day-of-month- value, the month-value, the year-value, the hour-value, the @@ -36,4 +32,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/11_7_return_the_parsed-cookie-date_as_the_result_of_thi.md b/notes/RFC/RFC6265/sections/11_7_return_the_parsed-cookie-date_as_the_result_of_thi.md index 79a54e453..3acf2a4f8 100644 --- a/notes/RFC/RFC6265/sections/11_7_return_the_parsed-cookie-date_as_the_result_of_thi.md +++ b/notes/RFC/RFC6265/sections/11_7_return_the_parsed-cookie-date_as_the_result_of_thi.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Return the parsed-cookie-date as the result of this algorithm." rfc_number: 6265 rfc_section: "7" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 7. Return the parsed-cookie-date as the result of this algorithm. - ### 5.1.2. Canonicalized Host Names A canonicalized host name is the string generated by the following @@ -50,9 +49,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat > **MUST**: The user agent MUST use an algorithm equivalent to the following algorithm to compute the default-path of a cookie: - - - 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise). For example, if the request-uri contains just a path (and optional query string), @@ -102,11 +98,8 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat interoperate with servers that do not follow the recommendations in Section 4. - - > **MUST**: A user agent MUST use an algorithm equivalent to the following algorithm to parse a "set-cookie-string": --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/12_1_if_the_set-cookie-string_contains_a_x3b_character.md b/notes/RFC/RFC6265/sections/12_1_if_the_set-cookie-string_contains_a_x3b_character.md index 37314d7ee..683a10d94 100644 --- a/notes/RFC/RFC6265/sections/12_1_if_the_set-cookie-string_contains_a_x3b_character.md +++ b/notes/RFC/RFC6265/sections/12_1_if_the_set-cookie-string_contains_a_x3b_character.md @@ -1,4 +1,4 @@ ---- +--- title: "1. If the set-cookie-string contains a %x3B (";") character:" rfc_number: 6265 rfc_section: "1" @@ -9,8 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 1. If the set-cookie-string contains a %x3B (";") character: - - The name-value-pair string consists of the characters up to, but not including, the first %x3B (";"), and the unparsed- attributes consist of the remainder of the set-cookie-string @@ -54,9 +52,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat Consume the characters of the unparsed-attributes up to, but not including, the first %x3B (";") character. - - - Otherwise: Consume the remainder of the unparsed-attributes. @@ -65,4 +60,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/13_4_if_the_cookie-av_string_contains_a_x3d_character.md b/notes/RFC/RFC6265/sections/13_4_if_the_cookie-av_string_contains_a_x3d_character.md index d986d887a..593e8d265 100644 --- a/notes/RFC/RFC6265/sections/13_4_if_the_cookie-av_string_contains_a_x3d_character.md +++ b/notes/RFC/RFC6265/sections/13_4_if_the_cookie-av_string_contains_a_x3d_character.md @@ -1,4 +1,4 @@ ---- +--- title: "4. If the cookie-av string contains a %x3D ("=") character:" rfc_number: 6265 rfc_section: "4" @@ -9,8 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 4. If the cookie-av string contains a %x3D ("=") character: - - The (possibly empty) attribute-name string consists of the characters up to, but not including, the first %x3D ("=") character, and the (possibly empty) attribute-value string @@ -31,4 +29,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/14_7_return_to_step_1_of_this_algorithm.md b/notes/RFC/RFC6265/sections/14_7_return_to_step_1_of_this_algorithm.md index b81e7e0fa..8424285f3 100644 --- a/notes/RFC/RFC6265/sections/14_7_return_to_step_1_of_this_algorithm.md +++ b/notes/RFC/RFC6265/sections/14_7_return_to_step_1_of_this_algorithm.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Return to Step 1 of this algorithm." rfc_number: 6265 rfc_section: "7" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 7. Return to Step 1 of this algorithm. - When the user agent finishes parsing the set-cookie-string, the user agent is said to "receive a cookie" from the request-uri with name cookie-name, value cookie-value, and attributes cookie-attribute- @@ -31,8 +30,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat > **MAY**: represent, the user agent MAY replace the expiry-time with the last representable date. - - If the expiry-time is earlier than the earliest date the user agent > **MAY**: can represent, the user agent MAY replace the expiry-time with the earliest representable date. @@ -82,8 +79,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat Append an attribute to the cookie-attribute-list with an attribute- name of Domain and an attribute-value of cookie-domain. - - ### 5.2.4. The Path Attribute If the attribute-name case-insensitively matches the string "Path", @@ -130,11 +125,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat from "third-party" responses or the user agent might not wish to store cookies that exceed some size. - - - - - 2. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time to the current date and time. @@ -184,8 +174,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat Let the domain-attribute be the empty string. - - Otherwise: Ignore the cookie entirely and abort these steps. @@ -202,4 +190,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/15_6_if_the_domain-attribute_is_non-empty.md b/notes/RFC/RFC6265/sections/15_6_if_the_domain-attribute_is_non-empty.md index 67d44c4cd..b53bc7eed 100644 --- a/notes/RFC/RFC6265/sections/15_6_if_the_domain-attribute_is_non-empty.md +++ b/notes/RFC/RFC6265/sections/15_6_if_the_domain-attribute_is_non-empty.md @@ -1,4 +1,4 @@ ---- +--- title: "6. If the domain-attribute is non-empty:" rfc_number: 6265 rfc_section: "6" @@ -9,8 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 6. If the domain-attribute is non-empty: - - If the canonicalized request-host does not domain-match the domain-attribute: @@ -42,10 +40,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat attribute-name of "HttpOnly", set the cookie's http-only-flag to true. Otherwise, set the cookie's http-only-flag to false. - - - - 10. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is set, abort these steps and ignore the cookie entirely. @@ -69,4 +63,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/16_12_insert_the_newly_created_cookie_into_the_cookie_st.md b/notes/RFC/RFC6265/sections/16_12_insert_the_newly_created_cookie_into_the_cookie_st.md index 978241fef..4ce52b748 100644 --- a/notes/RFC/RFC6265/sections/16_12_insert_the_newly_created_cookie_into_the_cookie_st.md +++ b/notes/RFC/RFC6265/sections/16_12_insert_the_newly_created_cookie_into_the_cookie_st.md @@ -1,4 +1,4 @@ ---- +--- title: "12. Insert the newly created cookie into the cookie store." rfc_number: 6265 rfc_section: "12" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 12. Insert the newly created cookie into the cookie store. - A cookie is "expired" if the cookie has an expiry date in the past. > **MUST**: The user agent MUST evict all expired cookies from the cookie store @@ -28,4 +27,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/17_1_expired_cookies.md b/notes/RFC/RFC6265/sections/17_1_expired_cookies.md index e4a52614e..3b2dd8756 100644 --- a/notes/RFC/RFC6265/sections/17_1_expired_cookies.md +++ b/notes/RFC/RFC6265/sections/17_1_expired_cookies.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Expired cookies." rfc_number: 6265 rfc_section: "1" @@ -9,10 +9,7 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 1. Expired cookies. - - number of other cookies. --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/18_3_all_cookies.md b/notes/RFC/RFC6265/sections/18_3_all_cookies.md index 690984e88..b86571c04 100644 --- a/notes/RFC/RFC6265/sections/18_3_all_cookies.md +++ b/notes/RFC/RFC6265/sections/18_3_all_cookies.md @@ -1,4 +1,4 @@ ---- +--- title: "3. All cookies." rfc_number: 6265 rfc_section: "3" @@ -9,12 +9,9 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 3. All cookies. - > **MUST**: If two cookies have the same removal priority, the user agent MUST evict the cookie with the earliest last-access date first. - - When "the current session is over" (as defined by the user agent), > **MUST**: the user agent MUST remove from the cookie store all cookies with the persistent-flag set to false. @@ -62,10 +59,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat this document. Typically, user agents consider a protocol secure if the protocol makes use of transport-layer - - - - security, such as SSL or TLS. For example, most user agents consider "https" to be a scheme that denotes a secure protocol. @@ -111,4 +104,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/19_6_implementation_considerations.md b/notes/RFC/RFC6265/sections/19_6_implementation_considerations.md index 5fbce0cc8..5990d1af1 100644 --- a/notes/RFC/RFC6265/sections/19_6_implementation_considerations.md +++ b/notes/RFC/RFC6265/sections/19_6_implementation_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Implementation Considerations" rfc_number: 6265 rfc_section: "6" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 6. Implementation Considerations - ## 6.1. Limits Practical user agent implementations have limits on the number and @@ -57,12 +56,9 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat > **SHOULD**: based domain name labels will exist in the wild. User agents SHOULD implement IDNA2008 [RFC5890] and MAY implement [UTS46] or [RFC5895] - - in order to facilitate their IDNA transition. If a user agent does > **MUST**: not implement IDNA2008, the user agent MUST implement IDNA2003 [RFC3490]. --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/20_7_privacy_considerations.md b/notes/RFC/RFC6265/sections/20_7_privacy_considerations.md index fdfef61d7..55d01651c 100644 --- a/notes/RFC/RFC6265/sections/20_7_privacy_considerations.md +++ b/notes/RFC/RFC6265/sections/20_7_privacy_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Privacy Considerations" rfc_number: 6265 rfc_section: "7" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 7. Privacy Considerations - Cookies are often criticized for letting servers track users. For example, a number of "web analytics" companies use cookies to recognize when a user returns to a web site or visits another web @@ -51,10 +50,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat cookies stored in the cookie store. For example, a user agent might let users delete all cookies received during a specified time period - - - - or all the cookies related to a particular domain. In addition, many user agents include a user interface element that lets users examine the cookies stored in their cookie store. @@ -87,4 +82,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/21_8_security_considerations.md b/notes/RFC/RFC6265/sections/21_8_security_considerations.md index 2d49701d4..f968eaae3 100644 --- a/notes/RFC/RFC6265/sections/21_8_security_considerations.md +++ b/notes/RFC/RFC6265/sections/21_8_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "8. Security Considerations" rfc_number: 6265 rfc_section: "8" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 8. Security Considerations - ## 8.1. Overview Cookies have a number of security pitfalls. This section overviews a @@ -26,9 +25,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat a victim's cookies because the cookie protocol itself has various vulnerabilities (see "Weak Confidentiality" and "Weak Integrity", - - - below). In addition, by default, cookies do not provide confidentiality or integrity from network attackers, even when used in conjunction with HTTPS. @@ -75,11 +71,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat 3. A malicious client could alter the Cookie header before transmission, with unpredictable results. - - - - - > **SHOULD**: Servers SHOULD encrypt and sign the contents of cookies (using whatever format the server desires) when transmitting them to the user agent (even when sending the cookies over a secure channel). @@ -129,8 +120,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat attacker transplants a session identifier from his or her user agent to the victim's user agent. Second, the victim uses that session - - identifier to interact with the server, possibly imbuing the session identifier with the user's credentials or confidential information. Third, the attacker uses the session identifier to interact with @@ -178,10 +167,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat able to leverage this ability to mount an attack against bar.example.com. - - - - Even though the Set-Cookie header supports the Path attribute, the Path attribute does not provide any integrity protection because the user agent will accept an arbitrary Path attribute in a Set-Cookie @@ -220,4 +205,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/22_9_iana_considerations.md b/notes/RFC/RFC6265/sections/22_9_iana_considerations.md index 6cc6aca1c..e6d1b54aa 100644 --- a/notes/RFC/RFC6265/sections/22_9_iana_considerations.md +++ b/notes/RFC/RFC6265/sections/22_9_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "9. IANA Considerations" rfc_number: 6265 rfc_section: "9" @@ -9,20 +9,9 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 9. IANA Considerations - The permanent message header field registry (see [RFC3864]) has been updated with the following registrations. - - - - - - - - - - ## 9.1. Cookie Header field name: Cookie @@ -73,4 +62,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/86_10_references.md b/notes/RFC/RFC6265/sections/86_10_references.md index e7bf8bc23..49c4c81c2 100644 --- a/notes/RFC/RFC6265/sections/86_10_references.md +++ b/notes/RFC/RFC6265/sections/86_10_references.md @@ -1,4 +1,4 @@ ---- +--- title: "10. References" rfc_number: 6265 rfc_section: "10" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # 10. References - ## 10.1. Normative References [RFC1034] Mockapetris, P., "Domain names - concepts and facilities", @@ -55,10 +54,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat [RFC2965] Kristol, D. and L. Montulli, "HTTP State Management Mechanism", RFC 2965, October 2000. - - - - [RFC2818] Rescorla, E., "HTTP Over TLS", RFC 2818, May 2000. [Netscape] Netscape Communications Corp., "Persistent Client State -- @@ -100,4 +95,3 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC6265/sections/99_appendix_a_acknowledgements.md b/notes/RFC/RFC6265/sections/99_appendix_a_acknowledgements.md index 678cc60f6..6da63cf2c 100644 --- a/notes/RFC/RFC6265/sections/99_appendix_a_acknowledgements.md +++ b/notes/RFC/RFC6265/sections/99_appendix_a_acknowledgements.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Acknowledgements" rfc_number: 6265 rfc_section: "Appendix A" @@ -9,7 +9,6 @@ tags: [RFC6265, cookies, state-management, Set-Cookie, domain-matching, path-mat # Appendix A. Acknowledgements - This document borrows heavily from RFC 2109 [RFC2109]. We are indebted to David M. Kristol and Lou Montulli for their efforts to specify cookies. David M. Kristol, in particular, provided @@ -33,4 +32,3 @@ Author's Address --- -**Navigation:** [[../RFC6265|RFC6265 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/RFC7541.md b/notes/RFC/RFC7541/RFC7541.md index ca28142e7..7bc518d28 100644 --- a/notes/RFC/RFC7541/RFC7541.md +++ b/notes/RFC/RFC7541/RFC7541.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 7541 — HPACK: Header Compression for HTTP/2" rfc_number: 7541 description: "HPACK header compression format for HTTP/2. Defines static table (61 entries), dynamic table with FIFO eviction, indexed/literal header representations, Huffman encoding, and table size management." @@ -11,17 +11,6 @@ aliases: [] **Official RFC**: [RFC 7541](https://www.rfc-editor.org/rfc/rfc7541) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 90/100 | -| **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC7541/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC7541/` — 7 files, 419 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC7541/` | -| **Key Gaps** | Large header bounds checking, header count limits, corrupted table recovery | - ## Core Concepts - [[RFC7541/sections/02_1_introduction|Introduction]] — Motivation for header compression in HTTP/2 @@ -33,29 +22,6 @@ aliases: [] - [[RFC7541/sections/91_appendix_a_static_table_definition|Static Table]] — 61-entry predefined table - [[RFC7541/sections/92_appendix_b_huffman_code|Huffman Code]] — Static Huffman encoding table -## Implementation Notes - -### Encoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC7541/HpackEncoder.cs` | HPACK header encoding with dynamic table | -| `Protocol/RFC7541/HuffmanCodec.cs` | Static Huffman encoding/decoding | - -### Decoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC7541/HpackDecoder.cs` | HPACK header decoding with dynamic table | -| `Protocol/RFC7541/HpackDynamicTable.cs` | FIFO dynamic table with 32-byte overhead | - -### Tests - -| Location | Count | Focus | -|----------|-------|-------| -| `TurboHTTP.Tests/RFC7541/` | 419 tests | HPACK encoding, decoding, table management | -| `TurboHTTP.StreamTests/RFC7541/` | — | HPACK stream integration tests | - ## Sections | # | Section | File | Status | @@ -82,7 +48,6 @@ aliases: [] ## See Also -- [[00-RFC_STATUS_MATRIX|RFC Status Matrix]] - [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps]] --- diff --git a/notes/RFC/RFC7541/sections/00_preamble.md b/notes/RFC/RFC7541/sections/00_preamble.md index e747ebf7f..9e3089406 100644 --- a/notes/RFC/RFC7541/sections/00_preamble.md +++ b/notes/RFC/RFC7541/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 7541 rfc_section: "preamble" @@ -9,19 +9,12 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # Preamble - - - - - - Internet Engineering Task Force (IETF) R. Peon Request for Comments: 7541 Google, Inc Category: Standards Track H. Ruellan ISSN: 2070-1721 Canon CRF May 2015 - HPACK: Header Compression for HTTP/2 Abstract @@ -58,14 +51,6 @@ Copyright Notice the Trust Legal Provisions and are provided without warranty as described in the Simplified BSD License. - - - - - - - - Table of Contents 1. Introduction ....................................................4 @@ -112,11 +97,6 @@ Table of Contents Appendix A. Static Table Definition ...............................25 Appendix B. Huffman Code ..........................................27 - - - - - Appendix C. Examples ..............................................33 C.1. Integer Representation Examples ............................33 C.1.1. Example 1: Encoding 10 Using a 5-Bit Prefix ............33 @@ -148,4 +128,3 @@ Table of Contents --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/02_1_introduction.md b/notes/RFC/RFC7541/sections/02_1_introduction.md index 069b4a738..849570403 100644 --- a/notes/RFC/RFC7541/sections/02_1_introduction.md +++ b/notes/RFC/RFC7541/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 7541 rfc_section: "1" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 1. Introduction - In HTTP/1.1 (see [RFC7230]), header fields are not compressed. As web pages have grown to require dozens to hundreds of requests, the redundant header fields in these requests unnecessarily consume @@ -57,8 +56,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, as new entries in the header field tables. The decoder executes the modifications to the header field tables prescribed by the encoder, - - reconstructing the list of header fields in the process. This enables decoders to remain simple and interoperate with a wide variety of encoders. @@ -106,4 +103,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/03_2_compression_process_overview.md b/notes/RFC/RFC7541/sections/03_2_compression_process_overview.md index a790b3f76..850bf0e8e 100644 --- a/notes/RFC/RFC7541/sections/03_2_compression_process_overview.md +++ b/notes/RFC/RFC7541/sections/03_2_compression_process_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Compression Process Overview" rfc_number: 7541 rfc_section: "2" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 2. Compression Process Overview - This specification does not describe a specific algorithm for an encoder. Instead, it defines precisely how a decoder is expected to operate, allowing encoders to produce any encoding that this @@ -57,8 +56,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, table is at the lowest index, and the oldest entry of a dynamic table is at the highest index. - - The dynamic table is initially empty. Entries are added as each header block is decompressed. @@ -103,13 +100,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Figure 1: Index Address Space - - - - - - - ## 2.4. Header Field Representation An encoded header field can be represented either as an index or as a @@ -150,4 +140,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/04_3_header_block_decoding.md b/notes/RFC/RFC7541/sections/04_3_header_block_decoding.md index 2d2eb4db1..249c376be 100644 --- a/notes/RFC/RFC7541/sections/04_3_header_block_decoding.md +++ b/notes/RFC/RFC7541/sections/04_3_header_block_decoding.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Header Block Decoding" rfc_number: 7541 rfc_section: "3" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 3. Header Block Decoding - ## 3.1. Header Block Processing A decoder processes a header block sequentially to reconstruct the @@ -19,8 +18,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, The different possible header field representations are described in Section 6. - - Once a header field is decoded and added to the reconstructed header list, the header field cannot be removed. A header field added to the header list can be safely passed to the application. @@ -62,4 +59,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/05_4_dynamic_table_management.md b/notes/RFC/RFC7541/sections/05_4_dynamic_table_management.md index 02841cc0d..750a9daab 100644 --- a/notes/RFC/RFC7541/sections/05_4_dynamic_table_management.md +++ b/notes/RFC/RFC7541/sections/05_4_dynamic_table_management.md @@ -1,4 +1,4 @@ ---- +--- title: "4. Dynamic Table Management" rfc_number: 7541 rfc_section: "4" @@ -9,17 +9,9 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 4. Dynamic Table Management - To limit the memory requirements on the decoder side, the dynamic table is constrained in size. - - - - - - - ## 4.1. Calculating Table Size The size of the dynamic table is the sum of the size of its entries. @@ -66,11 +58,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, dynamic table by setting a maximum size of 0, which can subsequently be restored. - - - - - ## 4.3. Entry Eviction When Dynamic Table Size Changes Whenever the maximum size for the dynamic table is reduced, entries @@ -98,4 +85,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/06_5_primitive_type_representations.md b/notes/RFC/RFC7541/sections/06_5_primitive_type_representations.md index 4540c8af1..b2301fa80 100644 --- a/notes/RFC/RFC7541/sections/06_5_primitive_type_representations.md +++ b/notes/RFC/RFC7541/sections/06_5_primitive_type_representations.md @@ -1,4 +1,4 @@ ---- +--- title: "5. Primitive Type Representations" rfc_number: 7541 rfc_section: "5" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 5. Primitive Type Representations - HPACK encoding uses two primitive types: unsigned variable-length integers and strings of octets. @@ -28,12 +27,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix. - - - - - - 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | ? | ? | ? | Value | @@ -84,11 +77,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, encode I on 8 bits ``` - - - - - Pseudocode to decode an integer I is as follows: decode I from the next N bits @@ -105,7 +93,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, return I ``` - Examples illustrating the encoding of integers are available in Appendix C.1. @@ -142,8 +129,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, literal, encoded as an integer with a 7-bit prefix (see Section 5.1). - - String Data: The encoded data of the string literal. If H is '0', then the encoded data is the raw octets of the string literal. If H is '1', then the encoded data is the Huffman encoding of the @@ -171,4 +156,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/07_6_binary_format.md b/notes/RFC/RFC7541/sections/07_6_binary_format.md index d43f85094..e55ee8b9e 100644 --- a/notes/RFC/RFC7541/sections/07_6_binary_format.md +++ b/notes/RFC/RFC7541/sections/07_6_binary_format.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Binary Format" rfc_number: 7541 rfc_section: "6" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 6. Binary Format - This section describes the detailed format of each of the different header field representations and the dynamic table size update instruction. @@ -29,11 +28,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Figure 5: Indexed Header Field - - - - - An indexed header field starts with the '1' 1-bit pattern, followed by the index of the matching header field, represented as an integer with a 7-bit prefix (see Section 5.1). @@ -69,22 +63,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Figure 6: Literal Header Field with Incremental Indexing -- Indexed Name - - - - - - - - - - - - - - - - 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | @@ -133,9 +111,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Figure 8: Literal Header Field without Indexing -- Indexed Name - - - 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | 0 | @@ -185,8 +160,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Figure 10: Literal Header Field Never Indexed -- Indexed Name - - 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 0 | @@ -234,10 +207,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, followed by the new maximum size, represented as an integer with a 5-bit prefix (see Section 5.1). - - - - > **MUST**: The new maximum size MUST be lower than or equal to the limit determined by the protocol using HPACK. A value that exceeds this > **MUST**: limit MUST be treated as a decoding error. In HTTP/2, this limit is @@ -250,4 +219,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/08_7_security_considerations.md b/notes/RFC/RFC7541/sections/08_7_security_considerations.md index c9c2c7f98..a82947693 100644 --- a/notes/RFC/RFC7541/sections/08_7_security_considerations.md +++ b/notes/RFC/RFC7541/sections/08_7_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Security Considerations" rfc_number: 7541 rfc_section: "7" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 7. Security Considerations - This section describes potential areas of security concern with HPACK: @@ -46,9 +45,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, given guess. Padding schemes also work directly against compression by increasing the number of bits that are transmitted. - - - Attacks like CRIME [CRIME] demonstrated the existence of these general attacker capabilities. The specific attack exploited the fact that DEFLATE [DEFLATE] removes redundancy based on prefix @@ -96,10 +92,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, of HTTP to take steps to mitigate attacks. It would impose new constraints on how HTTP is used. - - - - Rather than impose constraints on users of HTTP, an implementation of HPACK can instead constrain how compression is applied in order to limit the potential for dynamic table probing. @@ -145,12 +137,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, intermediaries that a particular value was intentionally sent as a literal. - - - - - - > **MUST NOT**: An intermediary MUST NOT re-encode a value that uses the never- indexed literal representation with another representation that would index it. If HPACK is used for re-encoding, the never-indexed @@ -197,11 +183,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, size of the data stored in the dynamic table, plus a small allowance for overhead. - - - - - A decoder can limit the amount of state memory used by setting an appropriate value for the maximum size of the dynamic table. In HTTP/2, this is realized by setting an appropriate value for the @@ -230,4 +211,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/86_8_references.md b/notes/RFC/RFC7541/sections/86_8_references.md index ecaa1e85d..f2fb8a157 100644 --- a/notes/RFC/RFC7541/sections/86_8_references.md +++ b/notes/RFC/RFC7541/sections/86_8_references.md @@ -1,4 +1,4 @@ ---- +--- title: "8. References" rfc_number: 7541 rfc_section: "8" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # 8. References - ## 8.1. Normative References [HTTP2] Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext @@ -27,12 +26,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, Routing", RFC 7230, DOI 10.17487/RFC7230, June 2014, . - - - - - - ## 8.2. Informative References [CANONICAL] Schwartz, E. and B. Kallick, "Generating a canonical @@ -73,4 +66,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/91_appendix_a_static_table_definition.md b/notes/RFC/RFC7541/sections/91_appendix_a_static_table_definition.md index c1fc2177c..cd64ef4bf 100644 --- a/notes/RFC/RFC7541/sections/91_appendix_a_static_table_definition.md +++ b/notes/RFC/RFC7541/sections/91_appendix_a_static_table_definition.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Static Table Definition" rfc_number: 7541 rfc_section: "Appendix A" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # Appendix A. Static Table Definition - The static table (see Section 2.3.1) consists in a predefined and unchangeable list of header fields. @@ -57,8 +56,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, | 29 | content-location | | | 30 | content-range | | - - | 31 | content-type | | | 32 | cookie | | | 33 | date | | @@ -96,4 +93,3 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/92_appendix_b_huffman_code.md b/notes/RFC/RFC7541/sections/92_appendix_b_huffman_code.md index 745cb487e..96ed55c4d 100644 --- a/notes/RFC/RFC7541/sections/92_appendix_b_huffman_code.md +++ b/notes/RFC/RFC7541/sections/92_appendix_b_huffman_code.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Huffman Code" rfc_number: 7541 rfc_section: "Appendix B" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # Appendix B. Huffman Code - The following Huffman code is used when encoding string literals with a Huffman coding (see Section 5.2). @@ -57,8 +56,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, ( 12) |11111111|11111111|11111110|1010 fffffea [28] ( 13) |11111111|11111111|11111111|111101 3ffffffd [30] - - ( 14) |11111111|11111111|11111110|1011 fffffeb [28] ( 15) |11111111|11111111|11111110|1100 fffffec [28] ( 16) |11111111|11111111|11111110|1101 fffffed [28] @@ -108,8 +105,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, '<' ( 60) |11111111|1111100 7ffc [15] '=' ( 61) |100000 20 [ 6] - - '>' ( 62) |11111111|1011 ffb [12] '?' ( 63) |11111111|00 3fc [10] '@' ( 64) |11111111|11010 1ffa [13] @@ -159,8 +154,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, 'l' (108) |101000 28 [ 6] 'm' (109) |101001 29 [ 6] - - 'n' (110) |101010 2a [ 6] 'o' (111) |00111 7 [ 5] 'p' (112) |101011 2b [ 6] @@ -210,8 +203,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, (156) |11111111|11111111|011001 3fffd9 [22] (157) |11111111|11111111|1100110 7fffe6 [23] - - (158) |11111111|11111111|1100111 7fffe7 [23] (159) |11111111|11111111|11101111 ffffef [24] (160) |11111111|11111111|011010 3fffda [22] @@ -261,8 +252,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, (204) |11111111|11111111|11111011|111 7ffffdf [27] (205) |11111111|11111111|11111001|01 3ffffe5 [26] - - (206) |11111111|11111111|11110001 fffff1 [24] (207) |11111111|11111111|11110110|1 1ffffed [25] (208) |11111111|11111110|010 7fff2 [19] @@ -312,12 +301,9 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, (252) |11111111|11111111|11111101|110 7ffffee [27] (253) |11111111|11111111|11111101|111 7ffffef [27] - - (254) |11111111|11111111|11111110|000 7fffff0 [27] (255) |11111111|11111111|11111011|10 3ffffee [26] EOS (256) |11111111|11111111|11111111|111111 3fffffff [30] --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7541/sections/93_appendix_c_examples.md b/notes/RFC/RFC7541/sections/93_appendix_c_examples.md index 781a61c6a..3f59dbe91 100644 --- a/notes/RFC/RFC7541/sections/93_appendix_c_examples.md +++ b/notes/RFC/RFC7541/sections/93_appendix_c_examples.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix C. Examples" rfc_number: 7541 rfc_section: "Appendix C" @@ -9,7 +9,6 @@ tags: [RFC7541, HPACK, header-compression, HTTP/2, dynamic-table, static-table, # Appendix C. Examples - This appendix contains examples covering integer encoding, header field representation, and the encoding of whole lists of header fields for both requests and responses, with and without Huffman @@ -40,12 +39,10 @@ C.1.2. Example 2: Encoding 1337 Using a 5-Bit Prefix The 5-bit prefix is filled with its max value (31). - ```abnf I = 1337 - (2^5 - 1) = 1306. ``` - I (1306) is greater than or equal to 128, so the while loop body executes: @@ -57,8 +54,6 @@ C.1.2. Example 2: Encoding 1337 Using a 5-Bit Prefix I is set to 10 (1306 / 128 == 10) - - I is no longer greater than or equal to 128, so the while loop terminates. @@ -104,12 +99,6 @@ C.2.1. Literal Header Field with Indexing 400a 6375 7374 6f6d 2d6b 6579 0d63 7573 | @.custom-key.cus 746f 6d2d 6865 6164 6572 | tom-header - - - - - - Decoding process: 40 | == Literal indexed == @@ -157,10 +146,6 @@ C.2.2. Literal Header Field without Indexing :path: /sample/path - - - - C.2.3. Literal Header Field Never Indexed The header field representation uses a literal name and a literal @@ -191,27 +176,6 @@ C.2.3. Literal Header Field Never Indexed password: secret - - - - - - - - - - - - - - - - - - - - - C.2.4. Indexed Header Field The header field representation uses an indexed header field from the @@ -256,13 +220,6 @@ C.3.1. First Request 8286 8441 0f77 7777 2e65 7861 6d70 6c65 | ...A.www.example 2e63 6f6d | .com - - - - - - - Decoding process: 82 | == Indexed - Add == @@ -308,12 +265,6 @@ C.3.2. Second Request 8286 84be 5808 6e6f 2d63 6163 6865 | ....X.no-cache - - - - - - Decoding process: 82 | == Indexed - Add == @@ -360,11 +311,6 @@ C.3.3. Third Request :authority: www.example.com custom-key: custom-value - - - - - Hex dump of encoded data: 8287 85bf 400a 6375 7374 6f6d 2d6b 6579 | ....@.custom-key @@ -408,14 +354,6 @@ C.3.3. Third Request :authority: www.example.com custom-key: custom-value - - - - - - - - C.4. Request Examples with Huffman Coding This section shows the same examples as the previous section but uses @@ -462,11 +400,6 @@ C.4.1. First Request [ 1] (s = 57) :authority: www.example.com Table size: 57 - - - - - Decoded header list: :method: GET @@ -513,11 +446,6 @@ C.4.2. Second Request | no-cache | -> cache-control: no-cache - - - - - Dynamic Table (after decoding): [ 1] (s = 53) cache-control: no-cache @@ -547,28 +475,6 @@ C.4.3. Third Request 8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925 | ....@.%.I.[.}..% a849 e95b b8e8 b4bf | .I.[.... - - - - - - - - - - - - - - - - - - - - - - Decoding process: 82 | == Indexed - Add == @@ -613,13 +519,6 @@ C.4.3. Third Request :authority: www.example.com custom-key: custom-value - - - - - - - C.5. Response Examples without Huffman Coding This section shows several consecutive header lists, corresponding to @@ -669,8 +568,6 @@ C.5.1. First Response 6e | == Literal indexed == | Indexed name (idx = 46) - - | location 17 | Literal value (len = 23) 6874 7470 733a 2f2f 7777 772e 6578 616d | https://www.exam @@ -720,8 +617,6 @@ C.5.2. Second Response | -> :status: 307 c1 | == Indexed - Add == - - | idx = 65 | -> cache-control: private c0 | == Indexed - Add == @@ -762,17 +657,6 @@ C.5.3. Third Response content-encoding: gzip set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 - - - - - - - - - - - Hex dump of encoded data: 88c1 611d 4d6f 6e2c 2032 3120 4f63 7420 | ..a.Mon, 21 Oct @@ -822,8 +706,6 @@ C.5.3. Third Response 206d 6178 2d61 6765 3d33 3630 303b 2076 | max-age=3600; v 6572 7369 6f6e 3d31 | ersion=1 - - | - evict: location: | https://www.example.com | - evict: :status: 307 @@ -873,8 +755,6 @@ C.6.1. First Response 2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8 | -..n..)...c..... e9ae 82ae 43d3 | ....C. - - Decoding process: 48 | == Literal indexed == @@ -919,13 +799,6 @@ C.6.1. First Response | -> location: | https://www.example.com - - - - - - - Dynamic Table (after decoding): [ 1] (s = 63) location: https://www.example.com @@ -975,8 +848,6 @@ C.6.2. Second Response c0 | == Indexed - Add == | idx = 64 - - | -> date: Mon, 21 Oct 2013 | 20:13:21 GMT bf | == Indexed - Add == @@ -1021,13 +892,6 @@ C.6.3. Third Response 3960 d5af 2708 7f36 72c1 ab27 0fb5 291f | 9`..'..6r..'..). 9587 3160 65c0 03ed 4ee5 b106 3d50 07 | ..1`e...N...=P. - - - - - - - Decoding process: 88 | == Indexed - Add == @@ -1077,8 +941,6 @@ C.6.3. Third Response | foo=ASDJKHQKBZXOQWEOPIUAXQ | WEOIU; max-age=3600; versi - - | on=1 | - evict: location: | https://www.example.com @@ -1104,32 +966,6 @@ C.6.3. Third Response content-encoding: gzip set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 - - - - - - - - - - - - - - - - - - - - - - - - - - Acknowledgments This specification includes substantial input from the following @@ -1142,4 +978,3 @@ Acknowledgments --- -**Navigation:** [[../RFC7541|RFC7541 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC7838/RFC7838.md b/notes/RFC/RFC7838/RFC7838.md index 891d3317a..0cb4e9656 100644 --- a/notes/RFC/RFC7838/RFC7838.md +++ b/notes/RFC/RFC7838/RFC7838.md @@ -1,4 +1,4 @@ -# RFC7838 – HTTP Alternative Services (Alt-Svc) +# RFC7838 – HTTP Alternative Services (Alt-Svc) **Status:** Full vault structure created for AltSvc test validation @@ -15,9 +15,9 @@ | Section | File | Focus | |---------|------|-------| -| 3 | `3_alt_svc_header_field.md` | Alt-Svc header syntax, parameters (ma, persist), clear value | -| 5 | `5_caching.md` | Caching rules, max-age persistence | -| 7 | `7_security.md` | Security considerations | +| 3 | [[RFC7838/sections/3_alt_svc_header_field\|3_alt_svc_header_field]] | Alt-Svc header syntax, parameters (ma, persist), clear value | +| 5 | [[RFC7838/sections/5_caching\|5_caching]] | Caching rules, max-age persistence | +| 7 | [[RFC7838/sections/7_security\|7_security]] | Security considerations | ## Test Trait Reference diff --git a/notes/RFC/RFC9000/RFC9000.md b/notes/RFC/RFC9000/RFC9000.md index c6f5baf8e..2b17a0162 100644 --- a/notes/RFC/RFC9000/RFC9000.md +++ b/notes/RFC/RFC9000/RFC9000.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport" rfc_number: 9000 description: "QUIC transport protocol over UDP with built-in TLS 1.3. TurboHTTP implements variable-length integer encoding and basic packet header parsing. Partial compliance — primitives only." @@ -11,17 +11,6 @@ aliases: [] **Official RFC**: [RFC 9000](https://www.rfc-editor.org/rfc/rfc9000) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 50/100 | -| **Implementation Status** | 🔶 Partial (primitives only) | -| **Implementation Path** | `TurboHTTP/Transport/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9114/` (shared with HTTP/3) | -| **Stream Test Files** | `TurboHTTP.StreamTests/IO/` | -| **Key Gaps** | Packet number management, loss detection, congestion control, connection migration, stateless reset | - ## Core Concepts - [[RFC9000/sections/02_1_overview|Overview]] — QUIC protocol goals and architecture @@ -33,28 +22,6 @@ aliases: [] - [[RFC9000/sections/45_17_2_long_header_packets|Long Header Packets]] — Initial, Handshake, 0-RTT, Retry packets - [[RFC9000/sections/46_17_3_short_header_packets|Short Header Packets]] — Application data packets -## Implementation Notes - -### Encoder - -| File | Purpose | -|------|---------| -| `Protocol/RFC9000/QuicVarInt.cs` | QUIC variable-length integer encoding/decoding | - -### Transport - -| File | Purpose | -|------|---------| -| `Transport/QuicConnectionManager.cs` | QUIC multi-stream manager | -| `Transport/QuicClientProvider.cs` | QUIC connection provider | - -### Tests - -| Location | Count | Focus | -|----------|-------|-------| -| `TurboHTTP.Tests/RFC9114/` | — | Shared with HTTP/3 tests | -| `TurboHTTP.StreamTests/IO/` | — | QUIC connection tests | - ## Sections | # | Section | File | Status | @@ -160,7 +127,6 @@ aliases: [] ## See Also -- [[00-RFC_STATUS_MATRIX|RFC Status Matrix]] - [[Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps]] --- diff --git a/notes/RFC/RFC9000/sections/00_preamble.md b/notes/RFC/RFC9000/sections/00_preamble.md index 551410f46..53dc899d4 100644 --- a/notes/RFC/RFC9000/sections/00_preamble.md +++ b/notes/RFC/RFC9000/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9000 rfc_section: "preamble" @@ -9,17 +9,12 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # Preamble - - - - Internet Engineering Task Force (IETF) J. Iyengar, Ed. Request for Comments: 9000 Fastly Category: Standards Track M. Thomson, Ed. ISSN: 2070-1721 Mozilla May 2021 - QUIC: A UDP-Based Multiplexed and Secure Transport Abstract @@ -267,4 +262,3 @@ Table of Contents --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/02_1_overview.md b/notes/RFC/RFC9000/sections/02_1_overview.md index 01e0d16ab..89fbde491 100644 --- a/notes/RFC/RFC9000/sections/02_1_overview.md +++ b/notes/RFC/RFC9000/sections/02_1_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Overview" rfc_number: 9000 rfc_section: "1" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 1. Overview - QUIC is a secure general-purpose transport protocol. This document defines version 1 of QUIC, which conforms to the version-independent properties of QUIC defined in [QUIC-INVARIANTS]. @@ -248,4 +247,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/03_2_streams.md b/notes/RFC/RFC9000/sections/03_2_streams.md index edcf53d74..4ca0aba09 100644 --- a/notes/RFC/RFC9000/sections/03_2_streams.md +++ b/notes/RFC/RFC9000/sections/03_2_streams.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Streams" rfc_number: 9000 rfc_section: "2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 2. Streams - Streams in QUIC provide a lightweight, ordered byte-stream abstraction to an application. Streams can be unidirectional or bidirectional. @@ -160,4 +159,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/04_3_1_sending_stream_states.md b/notes/RFC/RFC9000/sections/04_3_1_sending_stream_states.md index 744e9c7cb..91ddfff90 100644 --- a/notes/RFC/RFC9000/sections/04_3_1_sending_stream_states.md +++ b/notes/RFC/RFC9000/sections/04_3_1_sending_stream_states.md @@ -1,4 +1,4 @@ ---- +--- title: "3.1. Sending Stream States" rfc_number: 9000 rfc_section: "3.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 3.1. Sending Stream States - - This section describes streams in terms of their send or receive components. Two state machines are described: one for the streams on which an endpoint transmits data (Section 3.1) and another for @@ -134,4 +132,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/05_3_2_receiving_stream_states.md b/notes/RFC/RFC9000/sections/05_3_2_receiving_stream_states.md index 897249fae..2d0b433da 100644 --- a/notes/RFC/RFC9000/sections/05_3_2_receiving_stream_states.md +++ b/notes/RFC/RFC9000/sections/05_3_2_receiving_stream_states.md @@ -1,4 +1,4 @@ ---- +--- title: "3.2. Receiving Stream States" rfc_number: 9000 rfc_section: "3.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 3.2. Receiving Stream States - Figure 3 shows the states for the part of a stream that receives data from a peer. The states for a receiving part of a stream mirror only some of the states of the sending part of the stream at the peer. @@ -126,4 +125,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/06_3_3_permitted_frame_types.md b/notes/RFC/RFC9000/sections/06_3_3_permitted_frame_types.md index 509a8482e..775361105 100644 --- a/notes/RFC/RFC9000/sections/06_3_3_permitted_frame_types.md +++ b/notes/RFC/RFC9000/sections/06_3_3_permitted_frame_types.md @@ -1,4 +1,4 @@ ---- +--- title: "3.3. Permitted Frame Types" rfc_number: 9000 rfc_section: "3.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 3.3. Permitted Frame Types - The sender of a stream sends just three frame types that affect the state of a stream at either the sender or the receiver: STREAM (Section 19.8), STREAM_DATA_BLOCKED (Section 19.13), and RESET_STREAM @@ -36,4 +35,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/07_3_4_bidirectional_stream_states.md b/notes/RFC/RFC9000/sections/07_3_4_bidirectional_stream_states.md index ff3c03183..46ef55110 100644 --- a/notes/RFC/RFC9000/sections/07_3_4_bidirectional_stream_states.md +++ b/notes/RFC/RFC9000/sections/07_3_4_bidirectional_stream_states.md @@ -1,4 +1,4 @@ ---- +--- title: "3.4. Bidirectional Stream States" rfc_number: 9000 rfc_section: "3.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 3.4. Bidirectional Stream States - A bidirectional stream is composed of sending and receiving parts. Implementations can represent states of the bidirectional stream as composites of sending and receiving stream states. The simplest @@ -66,4 +65,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/08_3_5_solicited_state_transitions.md b/notes/RFC/RFC9000/sections/08_3_5_solicited_state_transitions.md index 26893fc9e..70039a7dd 100644 --- a/notes/RFC/RFC9000/sections/08_3_5_solicited_state_transitions.md +++ b/notes/RFC/RFC9000/sections/08_3_5_solicited_state_transitions.md @@ -1,4 +1,4 @@ ---- +--- title: "3.5. Solicited State Transitions" rfc_number: 9000 rfc_section: "3.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 3.5. Solicited State Transitions - If an application is no longer interested in the data it is receiving on a stream, it can abort reading the stream and specify an application error code. @@ -57,4 +56,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/09_4_flow_control.md b/notes/RFC/RFC9000/sections/09_4_flow_control.md index 0c488df31..38e5dcffd 100644 --- a/notes/RFC/RFC9000/sections/09_4_flow_control.md +++ b/notes/RFC/RFC9000/sections/09_4_flow_control.md @@ -1,4 +1,4 @@ ---- +--- title: "4. Flow Control" rfc_number: 9000 rfc_section: "4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 4. Flow Control - Receivers need to limit the amount of data that they are required to buffer, in order to prevent a fast sender from overwhelming them or a malicious sender from consuming a large amount of memory. To enable @@ -240,4 +239,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/10_5_1_connection_id.md b/notes/RFC/RFC9000/sections/10_5_1_connection_id.md index 12d81fa9d..e3ea7cc05 100644 --- a/notes/RFC/RFC9000/sections/10_5_1_connection_id.md +++ b/notes/RFC/RFC9000/sections/10_5_1_connection_id.md @@ -1,4 +1,4 @@ ---- +--- title: "5.1. Connection ID" rfc_number: 9000 rfc_section: "5.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 5.1. Connection ID - - A QUIC connection is shared state between a client and a server. Each connection starts with a handshake phase, during which the two @@ -220,4 +218,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/11_5_2_matching_packets_to_connections.md b/notes/RFC/RFC9000/sections/11_5_2_matching_packets_to_connections.md index b2ae6f0e1..4f54d4ed2 100644 --- a/notes/RFC/RFC9000/sections/11_5_2_matching_packets_to_connections.md +++ b/notes/RFC/RFC9000/sections/11_5_2_matching_packets_to_connections.md @@ -1,4 +1,4 @@ ---- +--- title: "5.2. Matching Packets to Connections" rfc_number: 9000 rfc_section: "5.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 5.2. Matching Packets to Connections - Incoming packets are classified on receipt. Packets can either be associated with an existing connection or -- for servers -- potentially create a new connection. @@ -132,4 +131,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/12_5_3_operations_on_connections.md b/notes/RFC/RFC9000/sections/12_5_3_operations_on_connections.md index 029d715fe..2a1fc0532 100644 --- a/notes/RFC/RFC9000/sections/12_5_3_operations_on_connections.md +++ b/notes/RFC/RFC9000/sections/12_5_3_operations_on_connections.md @@ -1,4 +1,4 @@ ---- +--- title: "5.3. Operations on Connections" rfc_number: 9000 rfc_section: "5.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 5.3. Operations on Connections - This document does not define an API for QUIC; it instead defines a set of functions for QUIC connections that application protocols can rely upon. An application protocol can assume that an implementation @@ -61,4 +60,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/13_6_version_negotiation.md b/notes/RFC/RFC9000/sections/13_6_version_negotiation.md index 8479c9135..c4d429507 100644 --- a/notes/RFC/RFC9000/sections/13_6_version_negotiation.md +++ b/notes/RFC/RFC9000/sections/13_6_version_negotiation.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Version Negotiation" rfc_number: 9000 rfc_section: "6" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 6. Version Negotiation - Version negotiation allows a server to indicate that it does not support the version the client used. A server sends a Version Negotiation packet in response to each packet that might initiate a @@ -83,4 +82,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/14_7_1_example_handshake_flows.md b/notes/RFC/RFC9000/sections/14_7_1_example_handshake_flows.md index b3ae46369..cee30dd37 100644 --- a/notes/RFC/RFC9000/sections/14_7_1_example_handshake_flows.md +++ b/notes/RFC/RFC9000/sections/14_7_1_example_handshake_flows.md @@ -1,4 +1,4 @@ ---- +--- title: "7.1. Example Handshake Flows" rfc_number: 9000 rfc_section: "7.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 7.1. Example Handshake Flows - - QUIC relies on a combined cryptographic and transport handshake to minimize connection establishment latency. QUIC uses the CRYPTO frame (Section 19.6) to transmit the cryptographic handshake. The @@ -148,4 +146,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/15_7_2_negotiating_connection_ids.md b/notes/RFC/RFC9000/sections/15_7_2_negotiating_connection_ids.md index e4633312d..3abbc0a77 100644 --- a/notes/RFC/RFC9000/sections/15_7_2_negotiating_connection_ids.md +++ b/notes/RFC/RFC9000/sections/15_7_2_negotiating_connection_ids.md @@ -1,4 +1,4 @@ ---- +--- title: "7.2. Negotiating Connection IDs" rfc_number: 9000 rfc_section: "7.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 7.2. Negotiating Connection IDs - A connection ID is used to ensure consistent routing of packets, as described in Section 5.1. The long header contains two connection IDs: the Destination Connection ID is chosen by the recipient of the @@ -75,4 +74,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/16_7_3_authenticating_connection_ids.md b/notes/RFC/RFC9000/sections/16_7_3_authenticating_connection_ids.md index bce7c0abd..50ca25a8f 100644 --- a/notes/RFC/RFC9000/sections/16_7_3_authenticating_connection_ids.md +++ b/notes/RFC/RFC9000/sections/16_7_3_authenticating_connection_ids.md @@ -1,4 +1,4 @@ ---- +--- title: "7.3. Authenticating Connection IDs" rfc_number: 9000 rfc_section: "7.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 7.3. Authenticating Connection IDs - The choice each endpoint makes about connection IDs during the handshake is authenticated by including all values in transport parameters; see Section 7.4. This ensures that all connection IDs @@ -105,4 +104,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/17_7_4_transport_parameters.md b/notes/RFC/RFC9000/sections/17_7_4_transport_parameters.md index 1411bff96..d248d3906 100644 --- a/notes/RFC/RFC9000/sections/17_7_4_transport_parameters.md +++ b/notes/RFC/RFC9000/sections/17_7_4_transport_parameters.md @@ -1,4 +1,4 @@ ---- +--- title: "7.4. Transport Parameters" rfc_number: 9000 rfc_section: "7.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 7.4. Transport Parameters - During connection establishment, both endpoints make authenticated declarations of their transport parameters. Endpoints are required to comply with the restrictions that each parameter defines; the @@ -167,4 +166,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/18_7_5_cryptographic_message_buffering.md b/notes/RFC/RFC9000/sections/18_7_5_cryptographic_message_buffering.md index a6bf04966..7f3815431 100644 --- a/notes/RFC/RFC9000/sections/18_7_5_cryptographic_message_buffering.md +++ b/notes/RFC/RFC9000/sections/18_7_5_cryptographic_message_buffering.md @@ -1,4 +1,4 @@ ---- +--- title: "7.5. Cryptographic Message Buffering" rfc_number: 9000 rfc_section: "7.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 7.5. Cryptographic Message Buffering - Implementations need to maintain a buffer of CRYPTO data received out of order. Because there is no flow control of CRYPTO frames, an endpoint could potentially force its peer to buffer an unbounded @@ -38,4 +37,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/19_8_1_address_validation_during_connection_establishment.md b/notes/RFC/RFC9000/sections/19_8_1_address_validation_during_connection_establishment.md index c36dfc6e3..9db994803 100644 --- a/notes/RFC/RFC9000/sections/19_8_1_address_validation_during_connection_establishment.md +++ b/notes/RFC/RFC9000/sections/19_8_1_address_validation_during_connection_establishment.md @@ -1,4 +1,4 @@ ---- +--- title: "8.1. Address Validation during Connection Establishment" rfc_number: 9000 rfc_section: "8.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 8.1. Address Validation during Connection Establishment - - Address validation ensures that an endpoint cannot be used for a traffic amplification attack. In such an attack, a packet is sent to a server with spoofed source address information that identifies a @@ -300,4 +298,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/20_8_2_path_validation.md b/notes/RFC/RFC9000/sections/20_8_2_path_validation.md index 32ab7f5ff..417211046 100644 --- a/notes/RFC/RFC9000/sections/20_8_2_path_validation.md +++ b/notes/RFC/RFC9000/sections/20_8_2_path_validation.md @@ -1,4 +1,4 @@ ---- +--- title: "8.2. Path Validation" rfc_number: 9000 rfc_section: "8.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 8.2. Path Validation - Path validation is used by both peers during connection migration (see Section 9) to verify reachability after a change of address. In path validation, endpoints test reachability between a specific local @@ -182,4 +181,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/21_9_1_probing_a_new_path.md b/notes/RFC/RFC9000/sections/21_9_1_probing_a_new_path.md index 1aa8d0b3d..017cb34b5 100644 --- a/notes/RFC/RFC9000/sections/21_9_1_probing_a_new_path.md +++ b/notes/RFC/RFC9000/sections/21_9_1_probing_a_new_path.md @@ -1,4 +1,4 @@ ---- +--- title: "9.1. Probing a New Path" rfc_number: 9000 rfc_section: "9.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.1. Probing a New Path - - The use of a connection ID allows connections to survive changes to endpoint addresses (IP address and port), such as those caused by an endpoint migrating to a new network. This section describes the @@ -69,4 +67,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/22_9_2_initiating_connection_migration.md b/notes/RFC/RFC9000/sections/22_9_2_initiating_connection_migration.md index d366a520b..93915114c 100644 --- a/notes/RFC/RFC9000/sections/22_9_2_initiating_connection_migration.md +++ b/notes/RFC/RFC9000/sections/22_9_2_initiating_connection_migration.md @@ -1,4 +1,4 @@ ---- +--- title: "9.2. Initiating Connection Migration" rfc_number: 9000 rfc_section: "9.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.2. Initiating Connection Migration - An endpoint can migrate a connection to a new local address by sending packets containing non-probing frames from that address. @@ -33,4 +32,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/23_9_3_responding_to_connection_migration.md b/notes/RFC/RFC9000/sections/23_9_3_responding_to_connection_migration.md index d5c44269f..03cea5e9f 100644 --- a/notes/RFC/RFC9000/sections/23_9_3_responding_to_connection_migration.md +++ b/notes/RFC/RFC9000/sections/23_9_3_responding_to_connection_migration.md @@ -1,4 +1,4 @@ ---- +--- title: "9.3. Responding to Connection Migration" rfc_number: 9000 rfc_section: "9.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.3. Responding to Connection Migration - Receiving a packet from a new peer address containing a non-probing frame indicates that the peer has migrated to that address. @@ -137,4 +136,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/24_9_4_loss_detection_and_congestion_control.md b/notes/RFC/RFC9000/sections/24_9_4_loss_detection_and_congestion_control.md index 885211c36..48a2ec981 100644 --- a/notes/RFC/RFC9000/sections/24_9_4_loss_detection_and_congestion_control.md +++ b/notes/RFC/RFC9000/sections/24_9_4_loss_detection_and_congestion_control.md @@ -1,4 +1,4 @@ ---- +--- title: "9.4. Loss Detection and Congestion Control" rfc_number: 9000 rfc_section: "9.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.4. Loss Detection and Congestion Control - The capacity available on the new path might not be the same as the > **MUST NOT**: old path. Packets sent on the old path MUST NOT contribute to congestion control or RTT estimation for the new path. @@ -53,4 +52,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/25_9_5_privacy_implications_of_connection_migration.md b/notes/RFC/RFC9000/sections/25_9_5_privacy_implications_of_connection_migration.md index f5607d99e..c924c2bb1 100644 --- a/notes/RFC/RFC9000/sections/25_9_5_privacy_implications_of_connection_migration.md +++ b/notes/RFC/RFC9000/sections/25_9_5_privacy_implications_of_connection_migration.md @@ -1,4 +1,4 @@ ---- +--- title: "9.5. Privacy Implications of Connection Migration" rfc_number: 9000 rfc_section: "9.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.5. Privacy Implications of Connection Migration - Using a stable connection ID on multiple network paths would allow a passive observer to correlate activity between those paths. An endpoint that moves between networks might not wish to have their @@ -82,4 +81,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/26_9_6_servers_preferred_address.md b/notes/RFC/RFC9000/sections/26_9_6_servers_preferred_address.md index d5702496c..5fff205d7 100644 --- a/notes/RFC/RFC9000/sections/26_9_6_servers_preferred_address.md +++ b/notes/RFC/RFC9000/sections/26_9_6_servers_preferred_address.md @@ -1,4 +1,4 @@ ---- +--- title: "9.6. Server's Preferred Address" rfc_number: 9000 rfc_section: "9.6" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.6. Server's Preferred Address - QUIC allows servers to accept connections on one IP address and attempt to transfer these connections to a more preferred address shortly after the handshake. This is particularly useful when @@ -112,4 +111,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/27_9_7_use_of_ipv6_flow_label_and_migration.md b/notes/RFC/RFC9000/sections/27_9_7_use_of_ipv6_flow_label_and_migration.md index ac4901e02..cd28ee9c4 100644 --- a/notes/RFC/RFC9000/sections/27_9_7_use_of_ipv6_flow_label_and_migration.md +++ b/notes/RFC/RFC9000/sections/27_9_7_use_of_ipv6_flow_label_and_migration.md @@ -1,4 +1,4 @@ ---- +--- title: "9.7. Use of IPv6 Flow Label and Migration" rfc_number: 9000 rfc_section: "9.7" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 9.7. Use of IPv6 Flow Label and Migration - > **SHOULD**: Endpoints that send data using IPv6 SHOULD apply an IPv6 flow label in compliance with [RFC6437], unless the local API does not allow setting IPv6 flow labels. @@ -28,4 +27,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/28_10_1_idle_timeout.md b/notes/RFC/RFC9000/sections/28_10_1_idle_timeout.md index 991f19971..915b102d0 100644 --- a/notes/RFC/RFC9000/sections/28_10_1_idle_timeout.md +++ b/notes/RFC/RFC9000/sections/28_10_1_idle_timeout.md @@ -1,4 +1,4 @@ ---- +--- title: "10.1. Idle Timeout" rfc_number: 9000 rfc_section: "10.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 10.1. Idle Timeout - - An established QUIC connection can be terminated in one of three ways: @@ -95,4 +93,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/29_10_2_immediate_close.md b/notes/RFC/RFC9000/sections/29_10_2_immediate_close.md index f32026bc9..fc0c60f62 100644 --- a/notes/RFC/RFC9000/sections/29_10_2_immediate_close.md +++ b/notes/RFC/RFC9000/sections/29_10_2_immediate_close.md @@ -1,4 +1,4 @@ ---- +--- title: "10.2. Immediate Close" rfc_number: 9000 rfc_section: "10.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 10.2. Immediate Close - An endpoint sends a CONNECTION_CLOSE frame (Section 19.19) to terminate the connection immediately. A CONNECTION_CLOSE frame causes all streams to immediately become closed; open streams can be @@ -194,4 +193,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/30_10_3_stateless_reset.md b/notes/RFC/RFC9000/sections/30_10_3_stateless_reset.md index f0c075551..c93db603c 100644 --- a/notes/RFC/RFC9000/sections/30_10_3_stateless_reset.md +++ b/notes/RFC/RFC9000/sections/30_10_3_stateless_reset.md @@ -1,4 +1,4 @@ ---- +--- title: "10.3. Stateless Reset" rfc_number: 9000 rfc_section: "10.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 10.3. Stateless Reset - A stateless reset is provided as an option of last resort for an endpoint that does not have access to the state of a connection. A crash or outage might result in peers continuing to send data to an @@ -261,4 +260,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/31_11_error_handling.md b/notes/RFC/RFC9000/sections/31_11_error_handling.md index 5d1e519d1..5ced60b22 100644 --- a/notes/RFC/RFC9000/sections/31_11_error_handling.md +++ b/notes/RFC/RFC9000/sections/31_11_error_handling.md @@ -1,4 +1,4 @@ ---- +--- title: "11. Error Handling" rfc_number: 9000 rfc_section: "11" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 11. Error Handling - > **SHOULD**: An endpoint that detects an error SHOULD signal the existence of that error to its peer. Both transport-level and application-level errors can affect an entire connection; see Section 11.1. Only application- @@ -89,4 +88,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/32_12_1_protected_packets.md b/notes/RFC/RFC9000/sections/32_12_1_protected_packets.md index 698e7233c..974be650e 100644 --- a/notes/RFC/RFC9000/sections/32_12_1_protected_packets.md +++ b/notes/RFC/RFC9000/sections/32_12_1_protected_packets.md @@ -1,4 +1,4 @@ ---- +--- title: "12.1. Protected Packets" rfc_number: 9000 rfc_section: "12.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 12.1. Protected Packets - - QUIC endpoints communicate by exchanging packets. Packets have confidentiality and integrity protection; see Section 12.1. Packets are carried in UDP datagrams; see Section 12.2. @@ -63,4 +61,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/33_12_2_coalescing_packets.md b/notes/RFC/RFC9000/sections/33_12_2_coalescing_packets.md index 00ef4a472..2248fd8c8 100644 --- a/notes/RFC/RFC9000/sections/33_12_2_coalescing_packets.md +++ b/notes/RFC/RFC9000/sections/33_12_2_coalescing_packets.md @@ -1,4 +1,4 @@ ---- +--- title: "12.2. Coalescing Packets" rfc_number: 9000 rfc_section: "12.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 12.2. Coalescing Packets - Initial (Section 17.2.2), 0-RTT (Section 17.2.3), and Handshake (Section 17.2.4) packets contain a Length field that determines the end of the packet. The length includes both the Packet Number and @@ -57,4 +56,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/34_12_3_packet_numbers.md b/notes/RFC/RFC9000/sections/34_12_3_packet_numbers.md index b1ad974e7..90533cb0b 100644 --- a/notes/RFC/RFC9000/sections/34_12_3_packet_numbers.md +++ b/notes/RFC/RFC9000/sections/34_12_3_packet_numbers.md @@ -1,4 +1,4 @@ ---- +--- title: "12.3. Packet Numbers" rfc_number: 9000 rfc_section: "12.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 12.3. Packet Numbers - The packet number is an integer in the range 0 to 2^62-1. This number is used in determining the cryptographic nonce for packet protection. Each endpoint maintains a separate packet number for @@ -81,4 +80,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/35_12_4_frames_and_frame_types.md b/notes/RFC/RFC9000/sections/35_12_4_frames_and_frame_types.md index dbaa0cf88..4d847e068 100644 --- a/notes/RFC/RFC9000/sections/35_12_4_frames_and_frame_types.md +++ b/notes/RFC/RFC9000/sections/35_12_4_frames_and_frame_types.md @@ -1,4 +1,4 @@ ---- +--- title: "12.4. Frames and Frame Types" rfc_number: 9000 rfc_section: "12.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 12.4. Frames and Frame Types - The payload of QUIC packets, after removing packet protection, consists of a sequence of complete frames, as shown in Figure 11. Version Negotiation, Stateless Reset, and Retry packets do not @@ -158,4 +157,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/36_12_5_frames_and_number_spaces.md b/notes/RFC/RFC9000/sections/36_12_5_frames_and_number_spaces.md index 3e9df467c..44087873d 100644 --- a/notes/RFC/RFC9000/sections/36_12_5_frames_and_number_spaces.md +++ b/notes/RFC/RFC9000/sections/36_12_5_frames_and_number_spaces.md @@ -1,4 +1,4 @@ ---- +--- title: "12.5. Frames and Number Spaces" rfc_number: 9000 rfc_section: "12.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 12.5. Frames and Number Spaces - Some frames are prohibited in different packet number spaces. The rules here generalize those of TLS, in that frames associated with establishing the connection can usually appear in packets in any @@ -39,4 +38,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/37_13_1_packet_processing.md b/notes/RFC/RFC9000/sections/37_13_1_packet_processing.md index a19839144..6878e80e8 100644 --- a/notes/RFC/RFC9000/sections/37_13_1_packet_processing.md +++ b/notes/RFC/RFC9000/sections/37_13_1_packet_processing.md @@ -1,4 +1,4 @@ ---- +--- title: "13.1. Packet Processing" rfc_number: 9000 rfc_section: "13.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 13.1. Packet Processing - - A sender sends one or more frames in a QUIC packet; see Section 12.4. A sender can minimize per-packet bandwidth and computational costs by @@ -57,4 +55,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/38_13_2_generating_acknowledgments.md b/notes/RFC/RFC9000/sections/38_13_2_generating_acknowledgments.md index 561b7e90b..f4bbf9ad2 100644 --- a/notes/RFC/RFC9000/sections/38_13_2_generating_acknowledgments.md +++ b/notes/RFC/RFC9000/sections/38_13_2_generating_acknowledgments.md @@ -1,4 +1,4 @@ ---- +--- title: "13.2. Generating Acknowledgments" rfc_number: 9000 rfc_section: "13.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 13.2. Generating Acknowledgments - Endpoints acknowledge all packets they receive and process. However, only ack-eliciting packets cause an ACK frame to be sent within the maximum ack delay. Packets that are not ack-eliciting are only @@ -248,4 +247,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/39_13_3_retransmission_of_information.md b/notes/RFC/RFC9000/sections/39_13_3_retransmission_of_information.md index b57309f45..3e9f36d2e 100644 --- a/notes/RFC/RFC9000/sections/39_13_3_retransmission_of_information.md +++ b/notes/RFC/RFC9000/sections/39_13_3_retransmission_of_information.md @@ -1,4 +1,4 @@ ---- +--- title: "13.3. Retransmission of Information" rfc_number: 9000 rfc_section: "13.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 13.3. Retransmission of Information - QUIC packets that are determined to be lost are not retransmitted whole. The same applies to the frames that are contained within lost packets. Instead, the information that might be carried in frames is @@ -142,4 +141,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/40_13_4_explicit_congestion_notification.md b/notes/RFC/RFC9000/sections/40_13_4_explicit_congestion_notification.md index 05cbde51e..7688b72ba 100644 --- a/notes/RFC/RFC9000/sections/40_13_4_explicit_congestion_notification.md +++ b/notes/RFC/RFC9000/sections/40_13_4_explicit_congestion_notification.md @@ -1,4 +1,4 @@ ---- +--- title: "13.4. Explicit Congestion Notification" rfc_number: 9000 rfc_section: "13.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 13.4. Explicit Congestion Notification - QUIC endpoints can use ECN [RFC3168] to detect and respond to network congestion. ECN allows an endpoint to set an ECN-Capable Transport (ECT) codepoint in the ECN field of an IP packet. A network node can @@ -154,4 +153,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/41_14_datagram_size.md b/notes/RFC/RFC9000/sections/41_14_datagram_size.md index 97adafc52..f24990be4 100644 --- a/notes/RFC/RFC9000/sections/41_14_datagram_size.md +++ b/notes/RFC/RFC9000/sections/41_14_datagram_size.md @@ -1,4 +1,4 @@ ---- +--- title: "14. Datagram Size" rfc_number: 9000 rfc_section: "14" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 14. Datagram Size - A UDP datagram can include one or more QUIC packets. The datagram size refers to the total UDP payload size of a single UDP datagram carrying QUIC packets. The datagram size includes one or more QUIC @@ -244,4 +243,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/42_15_versions.md b/notes/RFC/RFC9000/sections/42_15_versions.md index 5d8558e74..9cb894bf5 100644 --- a/notes/RFC/RFC9000/sections/42_15_versions.md +++ b/notes/RFC/RFC9000/sections/42_15_versions.md @@ -1,4 +1,4 @@ ---- +--- title: "15. Versions" rfc_number: 9000 rfc_section: "15" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 15. Versions - QUIC versions are identified using a 32-bit unsigned number. The version 0x00000000 is reserved to represent version negotiation. @@ -41,4 +40,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/43_16_variable-length_integer_encoding.md b/notes/RFC/RFC9000/sections/43_16_variable-length_integer_encoding.md index ef9219a0e..03f16f425 100644 --- a/notes/RFC/RFC9000/sections/43_16_variable-length_integer_encoding.md +++ b/notes/RFC/RFC9000/sections/43_16_variable-length_integer_encoding.md @@ -1,4 +1,4 @@ ---- +--- title: "16. Variable-Length Integer Encoding" rfc_number: 9000 rfc_section: "16" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 16. Variable-Length Integer Encoding - QUIC packets and frames commonly use a variable-length encoding for non-negative integer values. This encoding ensures that smaller integer values need fewer bytes to encode. @@ -51,4 +50,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/44_17_1_packet_number_encoding_and_decoding.md b/notes/RFC/RFC9000/sections/44_17_1_packet_number_encoding_and_decoding.md index 4346e4759..631d4dafd 100644 --- a/notes/RFC/RFC9000/sections/44_17_1_packet_number_encoding_and_decoding.md +++ b/notes/RFC/RFC9000/sections/44_17_1_packet_number_encoding_and_decoding.md @@ -1,4 +1,4 @@ ---- +--- title: "17.1. Packet Number Encoding and Decoding" rfc_number: 9000 rfc_section: "17.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 17.1. Packet Number Encoding and Decoding - - All numeric values are encoded in network byte order (that is, big endian), and all field sizes are in bits. Hexadecimal notation is used for describing the value of fields. @@ -63,4 +61,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/45_17_2_long_header_packets.md b/notes/RFC/RFC9000/sections/45_17_2_long_header_packets.md index 905a72a18..3aedd6e15 100644 --- a/notes/RFC/RFC9000/sections/45_17_2_long_header_packets.md +++ b/notes/RFC/RFC9000/sections/45_17_2_long_header_packets.md @@ -1,4 +1,4 @@ ---- +--- title: "17.2. Long Header Packets" rfc_number: 9000 rfc_section: "17.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 17.2. Long Header Packets - Long Header Packet { Header Form (1) = 1, Fixed Bit (1) = 1, @@ -531,4 +530,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/46_17_3_short_header_packets.md b/notes/RFC/RFC9000/sections/46_17_3_short_header_packets.md index 8eaffa2bb..8220066df 100644 --- a/notes/RFC/RFC9000/sections/46_17_3_short_header_packets.md +++ b/notes/RFC/RFC9000/sections/46_17_3_short_header_packets.md @@ -1,4 +1,4 @@ ---- +--- title: "17.3. Short Header Packets" rfc_number: 9000 rfc_section: "17.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 17.3. Short Header Packets - This version of QUIC defines a single packet type that uses the short packet header. @@ -90,4 +89,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/47_17_4_latency_spin_bit.md b/notes/RFC/RFC9000/sections/47_17_4_latency_spin_bit.md index 6542abf65..95d49394b 100644 --- a/notes/RFC/RFC9000/sections/47_17_4_latency_spin_bit.md +++ b/notes/RFC/RFC9000/sections/47_17_4_latency_spin_bit.md @@ -1,4 +1,4 @@ ---- +--- title: "17.4. Latency Spin Bit" rfc_number: 9000 rfc_section: "17.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 17.4. Latency Spin Bit - The latency spin bit, which is defined for 1-RTT packets (Section 17.3.1), enables passive latency monitoring from observation points on the network path throughout the duration of a connection. @@ -69,4 +68,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/48_18_transport_parameter_encoding.md b/notes/RFC/RFC9000/sections/48_18_transport_parameter_encoding.md index f1e294aac..99da8bc32 100644 --- a/notes/RFC/RFC9000/sections/48_18_transport_parameter_encoding.md +++ b/notes/RFC/RFC9000/sections/48_18_transport_parameter_encoding.md @@ -1,4 +1,4 @@ ---- +--- title: "18. Transport Parameter Encoding" rfc_number: 9000 rfc_section: "18" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 18. Transport Parameter Encoding - The extension_data field of the quic_transport_parameters extension defined in [QUIC-TLS] contains the QUIC transport parameters. They are encoded as a sequence of transport parameters, as shown in @@ -252,4 +251,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/49_19_1_padding_frames.md b/notes/RFC/RFC9000/sections/49_19_1_padding_frames.md index 4714ee665..ece5c00cd 100644 --- a/notes/RFC/RFC9000/sections/49_19_1_padding_frames.md +++ b/notes/RFC/RFC9000/sections/49_19_1_padding_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.1. PADDING Frames" rfc_number: 9000 rfc_section: "19.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.1. PADDING Frames - - As described in Section 12.4, packets contain one or more frames. This section describes the format and semantics of the core QUIC frame types. @@ -34,4 +32,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/50_19_2_ping_frames.md b/notes/RFC/RFC9000/sections/50_19_2_ping_frames.md index 4f7e61a4a..30b0cf30b 100644 --- a/notes/RFC/RFC9000/sections/50_19_2_ping_frames.md +++ b/notes/RFC/RFC9000/sections/50_19_2_ping_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.2. PING Frames" rfc_number: 9000 rfc_section: "19.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.2. PING Frames - Endpoints can use PING frames (type=0x01) to verify that their peers are still alive or to check reachability to the peer. @@ -31,4 +30,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/51_19_3_ack_frames.md b/notes/RFC/RFC9000/sections/51_19_3_ack_frames.md index 70adf71e9..c3330de76 100644 --- a/notes/RFC/RFC9000/sections/51_19_3_ack_frames.md +++ b/notes/RFC/RFC9000/sections/51_19_3_ack_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.3. ACK Frames" rfc_number: 9000 rfc_section: "19.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.3. ACK Frames - Receivers send ACK frames (types 0x02 and 0x03) to inform senders of packets they have received and processed. The ACK frame contains one or more ACK Ranges. ACK Ranges identify acknowledged packets. If @@ -123,12 +122,10 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat packet number for the range, the smallest value is determined by the following formula: - ```abnf smallest = largest - ack_range ``` - An ACK Range acknowledges all packets between the smallest packet number and the largest, inclusive. @@ -142,12 +139,10 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat The value of the Gap field establishes the largest packet number value for the subsequent ACK Range using the following formula: - ```abnf largest = previous_smallest - gap - 2 ``` - > **MUST**: If any computed packet number is negative, an endpoint MUST generate a connection error of type FRAME_ENCODING_ERROR. @@ -187,4 +182,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/52_19_4_reset_stream_frames.md b/notes/RFC/RFC9000/sections/52_19_4_reset_stream_frames.md index 1cb4fac96..21527f75b 100644 --- a/notes/RFC/RFC9000/sections/52_19_4_reset_stream_frames.md +++ b/notes/RFC/RFC9000/sections/52_19_4_reset_stream_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.4. RESET_STREAM Frames" rfc_number: 9000 rfc_section: "19.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.4. RESET_STREAM Frames - An endpoint uses a RESET_STREAM frame (type=0x04) to abruptly terminate the sending part of a stream. @@ -47,4 +46,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/53_19_5_stop_sending_frames.md b/notes/RFC/RFC9000/sections/53_19_5_stop_sending_frames.md index 926f24681..66448d20c 100644 --- a/notes/RFC/RFC9000/sections/53_19_5_stop_sending_frames.md +++ b/notes/RFC/RFC9000/sections/53_19_5_stop_sending_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.5. STOP_SENDING Frames" rfc_number: 9000 rfc_section: "19.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.5. STOP_SENDING Frames - An endpoint uses a STOP_SENDING frame (type=0x05) to communicate that incoming data is being discarded on receipt per application request. STOP_SENDING requests that a peer cease transmission on a stream. @@ -42,4 +41,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/54_19_6_crypto_frames.md b/notes/RFC/RFC9000/sections/54_19_6_crypto_frames.md index 64a076170..157f6e824 100644 --- a/notes/RFC/RFC9000/sections/54_19_6_crypto_frames.md +++ b/notes/RFC/RFC9000/sections/54_19_6_crypto_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.6. CRYPTO Frames" rfc_number: 9000 rfc_section: "19.6" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.6. CRYPTO Frames - A CRYPTO frame (type=0x06) is used to transmit cryptographic handshake messages. It can be sent in all packet types except 0-RTT. The CRYPTO frame offers the cryptographic protocol an in-order stream @@ -56,4 +55,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/55_19_7_new_token_frames.md b/notes/RFC/RFC9000/sections/55_19_7_new_token_frames.md index 4bf9886b1..99fa807bc 100644 --- a/notes/RFC/RFC9000/sections/55_19_7_new_token_frames.md +++ b/notes/RFC/RFC9000/sections/55_19_7_new_token_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.7. NEW_TOKEN Frames" rfc_number: 9000 rfc_section: "19.7" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.7. NEW_TOKEN Frames - A server sends a NEW_TOKEN frame (type=0x07) to provide the client with a token to send in the header of an Initial packet for a future connection. @@ -46,4 +45,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/56_19_8_stream_frames.md b/notes/RFC/RFC9000/sections/56_19_8_stream_frames.md index 5b0eb96ff..98f80a74f 100644 --- a/notes/RFC/RFC9000/sections/56_19_8_stream_frames.md +++ b/notes/RFC/RFC9000/sections/56_19_8_stream_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.8. STREAM Frames" rfc_number: 9000 rfc_section: "19.8" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.8. STREAM Frames - STREAM frames implicitly create a stream and carry stream data. The Type field in the STREAM frame takes the form 0b00001XXX (or the set of values from 0x08 to 0x0f). The three low-order bits of the frame @@ -77,4 +76,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/57_19_9_max_data_frames.md b/notes/RFC/RFC9000/sections/57_19_9_max_data_frames.md index 6b40b2481..bba68a47c 100644 --- a/notes/RFC/RFC9000/sections/57_19_9_max_data_frames.md +++ b/notes/RFC/RFC9000/sections/57_19_9_max_data_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.9. MAX_DATA Frames" rfc_number: 9000 rfc_section: "19.9" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.9. MAX_DATA Frames - A MAX_DATA frame (type=0x10) is used in flow control to inform the peer of the maximum amount of data that can be sent on the connection as a whole. @@ -39,4 +38,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/58_19_10_max_stream_data_frames.md b/notes/RFC/RFC9000/sections/58_19_10_max_stream_data_frames.md index 81bec32ad..718a2b9ec 100644 --- a/notes/RFC/RFC9000/sections/58_19_10_max_stream_data_frames.md +++ b/notes/RFC/RFC9000/sections/58_19_10_max_stream_data_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.10. MAX_STREAM_DATA Frames" rfc_number: 9000 rfc_section: "19.10" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.10. MAX_STREAM_DATA Frames - A MAX_STREAM_DATA frame (type=0x11) is used in flow control to inform a peer of the maximum amount of data that can be sent on a stream. @@ -55,4 +54,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/59_19_11_max_streams_frames.md b/notes/RFC/RFC9000/sections/59_19_11_max_streams_frames.md index c1a34b45d..a324d6996 100644 --- a/notes/RFC/RFC9000/sections/59_19_11_max_streams_frames.md +++ b/notes/RFC/RFC9000/sections/59_19_11_max_streams_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.11. MAX_STREAMS Frames" rfc_number: 9000 rfc_section: "19.11" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.11. MAX_STREAMS Frames - A MAX_STREAMS frame (type=0x12 or 0x13) informs the peer of the cumulative number of streams of a given type it is permitted to open. A MAX_STREAMS frame with a type of 0x12 applies to bidirectional @@ -54,4 +53,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/60_19_12_data_blocked_frames.md b/notes/RFC/RFC9000/sections/60_19_12_data_blocked_frames.md index c99897c9d..9df86a4eb 100644 --- a/notes/RFC/RFC9000/sections/60_19_12_data_blocked_frames.md +++ b/notes/RFC/RFC9000/sections/60_19_12_data_blocked_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.12. DATA_BLOCKED Frames" rfc_number: 9000 rfc_section: "19.12" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.12. DATA_BLOCKED Frames - > **SHOULD**: A sender SHOULD send a DATA_BLOCKED frame (type=0x14) when it wishes to send data but is unable to do so due to connection-level flow control; see Section 4. DATA_BLOCKED frames can be used as input to @@ -31,4 +30,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/61_19_13_stream_data_blocked_frames.md b/notes/RFC/RFC9000/sections/61_19_13_stream_data_blocked_frames.md index 1c96a8ba7..cfd3d68f8 100644 --- a/notes/RFC/RFC9000/sections/61_19_13_stream_data_blocked_frames.md +++ b/notes/RFC/RFC9000/sections/61_19_13_stream_data_blocked_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.13. STREAM_DATA_BLOCKED Frames" rfc_number: 9000 rfc_section: "19.13" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.13. STREAM_DATA_BLOCKED Frames - > **SHOULD**: A sender SHOULD send a STREAM_DATA_BLOCKED frame (type=0x15) when it wishes to send data but is unable to do so due to stream-level flow control. This frame is analogous to DATA_BLOCKED (Section 19.12). @@ -37,4 +36,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/62_19_14_streams_blocked_frames.md b/notes/RFC/RFC9000/sections/62_19_14_streams_blocked_frames.md index cd35eef65..829b8eb82 100644 --- a/notes/RFC/RFC9000/sections/62_19_14_streams_blocked_frames.md +++ b/notes/RFC/RFC9000/sections/62_19_14_streams_blocked_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.14. STREAMS_BLOCKED Frames" rfc_number: 9000 rfc_section: "19.14" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.14. STREAMS_BLOCKED Frames - > **SHOULD**: A sender SHOULD send a STREAMS_BLOCKED frame (type=0x16 or 0x17) when it wishes to open a stream but is unable to do so due to the maximum stream limit set by its peer; see Section 19.11. A STREAMS_BLOCKED @@ -41,4 +40,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/63_19_15_new_connection_id_frames.md b/notes/RFC/RFC9000/sections/63_19_15_new_connection_id_frames.md index f27ff3f98..a2b775bad 100644 --- a/notes/RFC/RFC9000/sections/63_19_15_new_connection_id_frames.md +++ b/notes/RFC/RFC9000/sections/63_19_15_new_connection_id_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.15. NEW_CONNECTION_ID Frames" rfc_number: 9000 rfc_section: "19.15" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.15. NEW_CONNECTION_ID Frames - An endpoint sends a NEW_CONNECTION_ID frame (type=0x18) to provide its peer with alternative connection IDs that can be used to break linkability when migrating connections; see Section 9.5. @@ -90,4 +89,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/64_19_16_retire_connection_id_frames.md b/notes/RFC/RFC9000/sections/64_19_16_retire_connection_id_frames.md index 10eb8d425..5dd439979 100644 --- a/notes/RFC/RFC9000/sections/64_19_16_retire_connection_id_frames.md +++ b/notes/RFC/RFC9000/sections/64_19_16_retire_connection_id_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.16. RETIRE_CONNECTION_ID Frames" rfc_number: 9000 rfc_section: "19.16" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.16. RETIRE_CONNECTION_ID Frames - An endpoint sends a RETIRE_CONNECTION_ID frame (type=0x19) to indicate that it will no longer use a connection ID that was issued by its peer. This includes the connection ID provided during the @@ -51,4 +50,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/65_19_17_path_challenge_frames.md b/notes/RFC/RFC9000/sections/65_19_17_path_challenge_frames.md index 3820ee6a4..01274cac0 100644 --- a/notes/RFC/RFC9000/sections/65_19_17_path_challenge_frames.md +++ b/notes/RFC/RFC9000/sections/65_19_17_path_challenge_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.17. PATH_CHALLENGE Frames" rfc_number: 9000 rfc_section: "19.17" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.17. PATH_CHALLENGE Frames - Endpoints can use PATH_CHALLENGE frames (type=0x1a) to check reachability to the peer and for path validation during connection migration. @@ -36,4 +35,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/66_19_18_path_response_frames.md b/notes/RFC/RFC9000/sections/66_19_18_path_response_frames.md index 2306267f0..817ad42a2 100644 --- a/notes/RFC/RFC9000/sections/66_19_18_path_response_frames.md +++ b/notes/RFC/RFC9000/sections/66_19_18_path_response_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.18. PATH_RESPONSE Frames" rfc_number: 9000 rfc_section: "19.18" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.18. PATH_RESPONSE Frames - A PATH_RESPONSE frame (type=0x1b) is sent in response to a PATH_CHALLENGE frame. @@ -30,4 +29,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/67_19_19_connection_close_frames.md b/notes/RFC/RFC9000/sections/67_19_19_connection_close_frames.md index 5e42c7a88..d958c2326 100644 --- a/notes/RFC/RFC9000/sections/67_19_19_connection_close_frames.md +++ b/notes/RFC/RFC9000/sections/67_19_19_connection_close_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.19. CONNECTION_CLOSE Frames" rfc_number: 9000 rfc_section: "19.19" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.19. CONNECTION_CLOSE Frames - An endpoint sends a CONNECTION_CLOSE frame (type=0x1c or 0x1d) to notify its peer that the connection is being closed. The CONNECTION_CLOSE frame with a type of 0x1c is used to signal errors @@ -66,4 +65,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/68_19_20_handshake_done_frames.md b/notes/RFC/RFC9000/sections/68_19_20_handshake_done_frames.md index e11e8ea82..21257b25b 100644 --- a/notes/RFC/RFC9000/sections/68_19_20_handshake_done_frames.md +++ b/notes/RFC/RFC9000/sections/68_19_20_handshake_done_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.20. HANDSHAKE_DONE Frames" rfc_number: 9000 rfc_section: "19.20" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.20. HANDSHAKE_DONE Frames - The server uses a HANDSHAKE_DONE frame (type=0x1e) to signal confirmation of the handshake to the client. @@ -29,4 +28,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/69_19_21_extension_frames.md b/notes/RFC/RFC9000/sections/69_19_21_extension_frames.md index f4a9bdbc9..d1ee1ea6a 100644 --- a/notes/RFC/RFC9000/sections/69_19_21_extension_frames.md +++ b/notes/RFC/RFC9000/sections/69_19_21_extension_frames.md @@ -1,4 +1,4 @@ ---- +--- title: "19.21. Extension Frames" rfc_number: 9000 rfc_section: "19.21" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 19.21. Extension Frames - QUIC frames do not use a self-describing encoding. An endpoint therefore needs to understand the syntax of all frames before it can successfully process a packet. This allows for efficient encoding of @@ -39,4 +38,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/70_20_error_codes.md b/notes/RFC/RFC9000/sections/70_20_error_codes.md index b9e6a658c..8cad545b3 100644 --- a/notes/RFC/RFC9000/sections/70_20_error_codes.md +++ b/notes/RFC/RFC9000/sections/70_20_error_codes.md @@ -1,4 +1,4 @@ ---- +--- title: "20. Error Codes" rfc_number: 9000 rfc_section: "20" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 20. Error Codes - QUIC transport error codes and application error codes are 62-bit unsigned integers. @@ -113,4 +112,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/71_21_1_overview_of_security_properties.md b/notes/RFC/RFC9000/sections/71_21_1_overview_of_security_properties.md index 2ab015cf1..219ea6497 100644 --- a/notes/RFC/RFC9000/sections/71_21_1_overview_of_security_properties.md +++ b/notes/RFC/RFC9000/sections/71_21_1_overview_of_security_properties.md @@ -1,4 +1,4 @@ ---- +--- title: "21.1. Overview of Security Properties" rfc_number: 9000 rfc_section: "21.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.1. Overview of Security Properties - - The goal of QUIC is to provide a secure transport connection. Section 21.1 provides an overview of those properties; subsequent sections discuss constraints and caveats regarding these properties, @@ -363,4 +361,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/72_21_2_handshake_denial_of_service.md b/notes/RFC/RFC9000/sections/72_21_2_handshake_denial_of_service.md index cf20f8547..54c2f68e4 100644 --- a/notes/RFC/RFC9000/sections/72_21_2_handshake_denial_of_service.md +++ b/notes/RFC/RFC9000/sections/72_21_2_handshake_denial_of_service.md @@ -1,4 +1,4 @@ ---- +--- title: "21.2. Handshake Denial of Service" rfc_number: 9000 rfc_section: "21.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.2. Handshake Denial of Service - As an encrypted and authenticated transport, QUIC provides a range of protections against denial of service. Once the cryptographic handshake is complete, QUIC endpoints discard most packets that are @@ -64,4 +63,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/73_21_3_amplification_attack.md b/notes/RFC/RFC9000/sections/73_21_3_amplification_attack.md index 9b79fdb4c..f6f19f541 100644 --- a/notes/RFC/RFC9000/sections/73_21_3_amplification_attack.md +++ b/notes/RFC/RFC9000/sections/73_21_3_amplification_attack.md @@ -1,4 +1,4 @@ ---- +--- title: "21.3. Amplification Attack" rfc_number: 9000 rfc_section: "21.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.3. Amplification Attack - An attacker might be able to receive an address validation token (Section 8) from a server and then release the IP address it used to acquire that token. At a later time, the attacker can initiate a @@ -23,4 +22,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/74_21_4_optimistic_ack_attack.md b/notes/RFC/RFC9000/sections/74_21_4_optimistic_ack_attack.md index e4b27beb3..6bde5abbe 100644 --- a/notes/RFC/RFC9000/sections/74_21_4_optimistic_ack_attack.md +++ b/notes/RFC/RFC9000/sections/74_21_4_optimistic_ack_attack.md @@ -1,4 +1,4 @@ ---- +--- title: "21.4. Optimistic ACK Attack" rfc_number: 9000 rfc_section: "21.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.4. Optimistic ACK Attack - An endpoint that acknowledges packets it has not received might cause a congestion controller to permit sending at rates beyond what the > **MAY**: network supports. An endpoint MAY skip packet numbers when sending @@ -19,4 +18,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/75_21_5_request_forgery_attacks.md b/notes/RFC/RFC9000/sections/75_21_5_request_forgery_attacks.md index aa9f204f3..86c2bc124 100644 --- a/notes/RFC/RFC9000/sections/75_21_5_request_forgery_attacks.md +++ b/notes/RFC/RFC9000/sections/75_21_5_request_forgery_attacks.md @@ -1,4 +1,4 @@ ---- +--- title: "21.5. Request Forgery Attacks" rfc_number: 9000 rfc_section: "21.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.5. Request Forgery Attacks - A request forgery attack occurs where an endpoint causes its peer to issue a request towards a victim, with the request controlled by the endpoint. Request forgery attacks aim to provide an attacker with @@ -261,4 +260,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/76_21_6_slowloris_attacks.md b/notes/RFC/RFC9000/sections/76_21_6_slowloris_attacks.md index ddcb04389..22b2167fb 100644 --- a/notes/RFC/RFC9000/sections/76_21_6_slowloris_attacks.md +++ b/notes/RFC/RFC9000/sections/76_21_6_slowloris_attacks.md @@ -1,4 +1,4 @@ ---- +--- title: "21.6. Slowloris Attacks" rfc_number: 9000 rfc_section: "21.6" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.6. Slowloris Attacks - The attacks commonly known as Slowloris [SLOWLORIS] try to keep many connections to the target endpoint open and hold them open as long as possible. These attacks can be executed against a QUIC endpoint by @@ -28,4 +27,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/77_21_7_stream_fragmentation_and_reassembly_attacks.md b/notes/RFC/RFC9000/sections/77_21_7_stream_fragmentation_and_reassembly_attacks.md index dc6389bf5..bba4c1973 100644 --- a/notes/RFC/RFC9000/sections/77_21_7_stream_fragmentation_and_reassembly_attacks.md +++ b/notes/RFC/RFC9000/sections/77_21_7_stream_fragmentation_and_reassembly_attacks.md @@ -1,4 +1,4 @@ ---- +--- title: "21.7. Stream Fragmentation and Reassembly Attacks" rfc_number: 9000 rfc_section: "21.7" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.7. Stream Fragmentation and Reassembly Attacks - An adversarial sender might intentionally not send portions of the stream data, causing the receiver to commit resources for the unsent data. This could cause a disproportionate receive buffer memory @@ -35,4 +34,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/78_21_8_stream_commitment_attack.md b/notes/RFC/RFC9000/sections/78_21_8_stream_commitment_attack.md index 9dd78af88..9183309d8 100644 --- a/notes/RFC/RFC9000/sections/78_21_8_stream_commitment_attack.md +++ b/notes/RFC/RFC9000/sections/78_21_8_stream_commitment_attack.md @@ -1,4 +1,4 @@ ---- +--- title: "21.8. Stream Commitment Attack" rfc_number: 9000 rfc_section: "21.8" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.8. Stream Commitment Attack - An adversarial endpoint can open a large number of streams, exhausting state on an endpoint. The adversarial endpoint could repeat the process on a large number of connections, in a manner @@ -34,4 +33,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/79_21_9_peer_denial_of_service.md b/notes/RFC/RFC9000/sections/79_21_9_peer_denial_of_service.md index 088cb77df..2dd070083 100644 --- a/notes/RFC/RFC9000/sections/79_21_9_peer_denial_of_service.md +++ b/notes/RFC/RFC9000/sections/79_21_9_peer_denial_of_service.md @@ -1,4 +1,4 @@ ---- +--- title: "21.9. Peer Denial of Service" rfc_number: 9000 rfc_section: "21.9" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.9. Peer Denial of Service - QUIC and TLS both contain frames or messages that have legitimate uses in some contexts, but these frames or messages can be abused to cause a peer to expend processing resources without having any @@ -31,4 +30,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/80_21_10_explicit_congestion_notification_attacks.md b/notes/RFC/RFC9000/sections/80_21_10_explicit_congestion_notification_attacks.md index c19172b2d..3eb06e96d 100644 --- a/notes/RFC/RFC9000/sections/80_21_10_explicit_congestion_notification_attacks.md +++ b/notes/RFC/RFC9000/sections/80_21_10_explicit_congestion_notification_attacks.md @@ -1,4 +1,4 @@ ---- +--- title: "21.10. Explicit Congestion Notification Attacks" rfc_number: 9000 rfc_section: "21.10" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.10. Explicit Congestion Notification Attacks - An on-path attacker could manipulate the value of ECN fields in the IP header to influence the sender's rate. [RFC3168] discusses manipulations and their effects in more detail. @@ -24,4 +23,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/81_21_11_stateless_reset_oracle.md b/notes/RFC/RFC9000/sections/81_21_11_stateless_reset_oracle.md index 5a405bcc3..62a6bd409 100644 --- a/notes/RFC/RFC9000/sections/81_21_11_stateless_reset_oracle.md +++ b/notes/RFC/RFC9000/sections/81_21_11_stateless_reset_oracle.md @@ -1,4 +1,4 @@ ---- +--- title: "21.11. Stateless Reset Oracle" rfc_number: 9000 rfc_section: "21.11" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.11. Stateless Reset Oracle - Stateless resets create a possible denial-of-service attack analogous to a TCP reset injection. This attack is possible if an attacker is able to cause a stateless reset token to be generated for a @@ -42,4 +41,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/82_21_12_version_downgrade.md b/notes/RFC/RFC9000/sections/82_21_12_version_downgrade.md index 8ea5d3989..808239798 100644 --- a/notes/RFC/RFC9000/sections/82_21_12_version_downgrade.md +++ b/notes/RFC/RFC9000/sections/82_21_12_version_downgrade.md @@ -1,4 +1,4 @@ ---- +--- title: "21.12. Version Downgrade" rfc_number: 9000 rfc_section: "21.12" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.12. Version Downgrade - This document defines QUIC Version Negotiation packets (Section 6), which can be used to negotiate the QUIC version used between two endpoints. However, this document does not specify how this @@ -21,4 +20,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/83_21_13_targeted_attacks_by_routing.md b/notes/RFC/RFC9000/sections/83_21_13_targeted_attacks_by_routing.md index dbefdf3a2..b6270687e 100644 --- a/notes/RFC/RFC9000/sections/83_21_13_targeted_attacks_by_routing.md +++ b/notes/RFC/RFC9000/sections/83_21_13_targeted_attacks_by_routing.md @@ -1,4 +1,4 @@ ---- +--- title: "21.13. Targeted Attacks by Routing" rfc_number: 9000 rfc_section: "21.13" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.13. Targeted Attacks by Routing - Deployments should limit the ability of an attacker to target a new connection to a particular server instance. Ideally, routing decisions are made independently of client-selected values, including @@ -18,4 +17,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/84_21_14_traffic_analysis.md b/notes/RFC/RFC9000/sections/84_21_14_traffic_analysis.md index 1e5b850a9..f72c569b9 100644 --- a/notes/RFC/RFC9000/sections/84_21_14_traffic_analysis.md +++ b/notes/RFC/RFC9000/sections/84_21_14_traffic_analysis.md @@ -1,4 +1,4 @@ ---- +--- title: "21.14. Traffic Analysis" rfc_number: 9000 rfc_section: "21.14" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 21.14. Traffic Analysis - The length of QUIC packets can reveal information about the length of the content of those packets. The PADDING frame is provided so that endpoints have some ability to obscure the length of packet content; @@ -22,4 +21,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/85_22_1_registration_policies_for_quic_registries.md b/notes/RFC/RFC9000/sections/85_22_1_registration_policies_for_quic_registries.md index c1ecddc0e..2f52385b7 100644 --- a/notes/RFC/RFC9000/sections/85_22_1_registration_policies_for_quic_registries.md +++ b/notes/RFC/RFC9000/sections/85_22_1_registration_policies_for_quic_registries.md @@ -1,4 +1,4 @@ ---- +--- title: "22.1. Registration Policies for QUIC Registries" rfc_number: 9000 rfc_section: "22.1" @@ -9,8 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 22.1. Registration Policies for QUIC Registries - - This document establishes several registries for the management of codepoints in QUIC. These registries operate on a common set of policies as defined in Section 22.1. @@ -145,4 +143,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/86_22_2_quic_versions_registry.md b/notes/RFC/RFC9000/sections/86_22_2_quic_versions_registry.md index a682a620a..4776e9f3d 100644 --- a/notes/RFC/RFC9000/sections/86_22_2_quic_versions_registry.md +++ b/notes/RFC/RFC9000/sections/86_22_2_quic_versions_registry.md @@ -1,4 +1,4 @@ ---- +--- title: "22.2. QUIC Versions Registry" rfc_number: 9000 rfc_section: "22.2" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 22.2. QUIC Versions Registry - IANA has added a registry for "QUIC Versions" under a "QUIC" heading. The "QUIC Versions" registry governs a 32-bit space; see Section 15. @@ -29,4 +28,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/87_22_3_quic_transport_parameters_registry.md b/notes/RFC/RFC9000/sections/87_22_3_quic_transport_parameters_registry.md index 1fe1bcb5d..6caa06908 100644 --- a/notes/RFC/RFC9000/sections/87_22_3_quic_transport_parameters_registry.md +++ b/notes/RFC/RFC9000/sections/87_22_3_quic_transport_parameters_registry.md @@ -1,4 +1,4 @@ ---- +--- title: "22.3. QUIC Transport Parameters Registry" rfc_number: 9000 rfc_section: "22.3" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 22.3. QUIC Transport Parameters Registry - IANA has added a registry for "QUIC Transport Parameters" under a "QUIC" heading. @@ -74,4 +73,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/88_22_4_quic_frame_types_registry.md b/notes/RFC/RFC9000/sections/88_22_4_quic_frame_types_registry.md index c551f39a3..6b9e21600 100644 --- a/notes/RFC/RFC9000/sections/88_22_4_quic_frame_types_registry.md +++ b/notes/RFC/RFC9000/sections/88_22_4_quic_frame_types_registry.md @@ -1,4 +1,4 @@ ---- +--- title: "22.4. QUIC Frame Types Registry" rfc_number: 9000 rfc_section: "22.4" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 22.4. QUIC Frame Types Registry - IANA has added a registry for "QUIC Frame Types" under a "QUIC" heading. @@ -40,4 +39,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/89_22_5_quic_transport_error_codes_registry.md b/notes/RFC/RFC9000/sections/89_22_5_quic_transport_error_codes_registry.md index 57865a0fd..99f6644c5 100644 --- a/notes/RFC/RFC9000/sections/89_22_5_quic_transport_error_codes_registry.md +++ b/notes/RFC/RFC9000/sections/89_22_5_quic_transport_error_codes_registry.md @@ -1,4 +1,4 @@ ---- +--- title: "22.5. QUIC Transport Error Codes Registry" rfc_number: 9000 rfc_section: "22.5" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 22.5. QUIC Transport Error Codes Registry - IANA has added a registry for "QUIC Transport Error Codes" under a "QUIC" heading. @@ -99,4 +98,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/90_23_references.md b/notes/RFC/RFC9000/sections/90_23_references.md index 7aa0bcc13..6f3d8617e 100644 --- a/notes/RFC/RFC9000/sections/90_23_references.md +++ b/notes/RFC/RFC9000/sections/90_23_references.md @@ -1,4 +1,4 @@ ---- +--- title: "23. References" rfc_number: 9000 rfc_section: "23" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # 23. References - ## 23.1. Normative References [BCP38] Ferguson, P. and D. Senie, "Network Ingress Filtering: @@ -242,4 +241,3 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9000/sections/91_appendix_a_pseudocode.md b/notes/RFC/RFC9000/sections/91_appendix_a_pseudocode.md index 33f7c5882..956a3557a 100644 --- a/notes/RFC/RFC9000/sections/91_appendix_a_pseudocode.md +++ b/notes/RFC/RFC9000/sections/91_appendix_a_pseudocode.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Pseudocode" rfc_number: 9000 rfc_section: "Appendix A" @@ -9,7 +9,6 @@ tags: [RFC9000, QUIC, transport, UDP, variable-length-integer, connection-migrat # Appendix A. Pseudocode - The pseudocode in this section describes sample algorithms. These algorithms are intended to be correct and clear, rather than being optimally performant. @@ -34,7 +33,6 @@ A.1. Sample Variable-Length Integer Decoding length = 1 << prefix ``` - // Once the length is known, remove these bits and read any // remaining bytes. @@ -233,4 +231,3 @@ Contributors --- -**Navigation:** [[../RFC9000|RFC9000 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/RFC9110.md b/notes/RFC/RFC9110/RFC9110.md index c8511baec..5225cd32e 100644 --- a/notes/RFC/RFC9110/RFC9110.md +++ b/notes/RFC/RFC9110/RFC9110.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9110 — HTTP Semantics" rfc_number: 9110 description: "Core HTTP semantics shared by all versions. Defines methods, status codes, content negotiation, redirects, idempotent retry logic, and authentication framework." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9110" **Official RFC**: [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 82/100 | -| **Implementation Status** | 🔶 Partial | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9110/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9110/` — 2 files, 123 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9110/` | -| **Key Gaps** | HTTPS→HTTP redirect protection, redirect loop detection, server-driven content negotiation limits | - ## Core Concepts - [[RFC9110/sections/44_9_1_overview|§9.1 Method Overview]] — method definitions and semantics @@ -34,31 +23,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9110" - [[RFC9110/sections/41_8_6_content-length|§8.6 Content-Length]] — message body framing - [[RFC9110/sections/49_11_1_authentication_scheme|§11.1 Authentication]] — authentication framework -## Implementation Notes - -### Business Logic - -| Component | File | Purpose | -|-----------|------|---------| -| `RedirectHandler` | `Protocol/RFC9110/RedirectHandler.cs` | §15.4 redirect following with method rewriting | -| `RetryEvaluator` | `Protocol/RFC9110/RetryEvaluator.cs` | §9.2 idempotency-based retry, Retry-After parsing | -| `ContentEncodingDecoder` | `Protocol/RFC9110/ContentEncodingDecoder.cs` | §8.4 gzip/deflate/brotli decompression | - -### Stages - -| Stage | File | Purpose | -|-------|------|---------| -| `RedirectBidiStage` | `Streams/Stages/Features/RedirectBidiStage.cs` | Redirect following in stream pipeline | -| `RetryBidiStage` | `Streams/Stages/Features/RetryBidiStage.cs` | Idempotent retry in stream pipeline | -| `DecompressionBidiStage` | `Streams/Stages/Features/DecompressionBidiStage.cs` | Response decompression in stream pipeline | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/RFC9110/` | 123 unit tests — redirect, retry, decompression | -| `TurboHTTP.StreamTests/RFC9110/` | Stage behaviour tests — redirect, retry, decompression stages | - ## Sections | # | Section | File | Status | @@ -124,7 +88,13 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9110" | 59 | 12.4 Content Negotiation Features | [[RFC9110/sections/59_12_4_content_negotiation_field_features\|59_12_4_features]] | ✅ | | 60 | 12.5 Content Negotiation Fields | [[RFC9110/sections/60_12_5_content_negotiation_fields\|60_12_5_fields]] | ✅ | | 61 | 13 Conditional Requests | [[RFC9110/sections/61_13_conditional_requests\|61_13_conditional]] | ✅ | -| 62–68 | 13.x Condition Evaluation | [[RFC9110/sections/62_3_otherwise_the_condition_is_false\|62–68 conditions]] | ✅ | +| 62 | 13.x Step 3 – Condition false (If-Match) | [[RFC9110/sections/62_3_otherwise_the_condition_is_false\|62_condition_false]] | ✅ | +| 63 | 13.x Step 3 – Condition true (If-None-Match) | [[RFC9110/sections/63_3_otherwise_the_condition_is_true\|63_condition_true]] | ✅ | +| 64 | 13.x Step 2 – Condition true (If-Modified-Since) | [[RFC9110/sections/64_2_otherwise_the_condition_is_true\|64_condition_true]] | ✅ | +| 65 | 13.x Step 2 – Condition false (If-Unmodified-Since) | [[RFC9110/sections/65_2_otherwise_the_condition_is_false\|65_condition_false]] | ✅ | +| 66 | 13.x Step 3 – Condition false (If-Unmodified-Since) | [[RFC9110/sections/66_3_otherwise_the_condition_is_false\|66_condition_false]] | ✅ | +| 67 | 13.x Step 2 – Condition false (If-Range) | [[RFC9110/sections/67_2_otherwise_the_condition_is_false\|67_condition_false]] | ✅ | +| 68 | 13.x Step 6 – Otherwise (combined evaluation) | [[RFC9110/sections/68_6_otherwise\|68_otherwise]] | ✅ | | 69 | 14.1 Range Units | [[RFC9110/sections/69_14_1_range_units\|69_14_1_range_units]] | ✅ | | 70 | 14.2 Range | [[RFC9110/sections/70_14_2_range\|70_14_2_range]] | ✅ | | 71 | 14.3 Accept-Ranges | [[RFC9110/sections/71_14_3_accept-ranges\|71_14_3_accept_ranges]] | ✅ | @@ -144,7 +114,8 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9110" | 85 | 16.5 Range Unit Extensibility | [[RFC9110/sections/85_16_5_range_unit_extensibility\|85_16_5_range_ext]] | ✅ | | 86 | 16.6 Content Coding Extensibility | [[RFC9110/sections/86_16_6_content_coding_extensibility\|86_16_6_coding_ext]] | ✅ | | 87 | 16.7 Upgrade Token Registry | [[RFC9110/sections/87_16_7_upgrade_token_registry\|87_16_7_upgrade_registry]] | ✅ | -| 88–89 | Protocol Registration | [[RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist\|88–89 registration]] | ✅ | +| 88 | 16.7 Protocol Registration – Token persistence | [[RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist\|88_registration_token]] | ✅ | +| 89 | 16.7 Protocol Registration – Point of contact | [[RFC9110/sections/89_4_the_registration_must_name_a_point_of_contact\|89_registration_contact]] | ✅ | | 90 | 17.1 Establishing Authority | [[RFC9110/sections/90_17_1_establishing_authority\|90_17_1_authority]] | ✅ | | 91 | 17.2 Risks of Intermediaries | [[RFC9110/sections/91_17_2_risks_of_intermediaries\|91_17_2_intermediary_risks]] | ✅ | | 92 | 17.3 File/Path Name Attacks | [[RFC9110/sections/92_17_3_attacks_based_on_file_and_path_names\|92_17_3_path_attacks]] | ✅ | @@ -185,7 +156,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9110" - [[RFC9113/RFC9113|RFC 9113 — HTTP/2]] — binary framing protocol - [[RFC9114/RFC9114|RFC 9114 — HTTP/3]] — HTTP over QUIC - [[RFC9111/RFC9111|RFC 9111 — HTTP Caching]] — caching model -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking --- diff --git a/notes/RFC/RFC9110/sections/00_preamble.md b/notes/RFC/RFC9110/sections/00_preamble.md index 71c756417..6af2dcbdd 100644 --- a/notes/RFC/RFC9110/sections/00_preamble.md +++ b/notes/RFC/RFC9110/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9110 rfc_section: "preamble" @@ -9,10 +9,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ## Preamble - - - - Internet Engineering Task Force (IETF) R. Fielding, Ed. Request for Comments: 9110 Adobe STD: 97 M. Nottingham, Ed. @@ -22,7 +18,6 @@ Updates: 3864 greenbytes Category: Standards Track June 2022 ISSN: 2070-1721 - HTTP Semantics Abstract @@ -390,4 +385,3 @@ Table of Contents --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/02_1_introduction.md b/notes/RFC/RFC9110/sections/02_1_introduction.md index b0386907b..f818dd7d9 100644 --- a/notes/RFC/RFC9110/sections/02_1_introduction.md +++ b/notes/RFC/RFC9110/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9110 rfc_section: "1" @@ -147,4 +147,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/03_2_conformance.md b/notes/RFC/RFC9110/sections/03_2_conformance.md index 52993acaf..f4c6d90dc 100644 --- a/notes/RFC/RFC9110/sections/03_2_conformance.md +++ b/notes/RFC/RFC9110/sections/03_2_conformance.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Conformance" rfc_number: 9110 rfc_section: "2" @@ -185,4 +185,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/04_3_1_resources.md b/notes/RFC/RFC9110/sections/04_3_1_resources.md index c1703af2b..be76bd260 100644 --- a/notes/RFC/RFC9110/sections/04_3_1_resources.md +++ b/notes/RFC/RFC9110/sections/04_3_1_resources.md @@ -1,4 +1,4 @@ ---- +--- title: "3.1. Resources" rfc_number: 9110 rfc_section: "3.1" @@ -40,4 +40,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/05_3_2_representations.md b/notes/RFC/RFC9110/sections/05_3_2_representations.md index 6151cd32a..2f36e3b7b 100644 --- a/notes/RFC/RFC9110/sections/05_3_2_representations.md +++ b/notes/RFC/RFC9110/sections/05_3_2_representations.md @@ -1,4 +1,4 @@ ---- +--- title: "3.2. Representations" rfc_number: 9110 rfc_section: "3.2" @@ -46,4 +46,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/06_3_3_connections_clients_and_servers.md b/notes/RFC/RFC9110/sections/06_3_3_connections_clients_and_servers.md index ac5a91ab0..f854c82c9 100644 --- a/notes/RFC/RFC9110/sections/06_3_3_connections_clients_and_servers.md +++ b/notes/RFC/RFC9110/sections/06_3_3_connections_clients_and_servers.md @@ -1,4 +1,4 @@ ---- +--- title: "3.3. Connections, Clients, and Servers" rfc_number: 9110 rfc_section: "3.3" @@ -41,4 +41,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/07_3_4_messages.md b/notes/RFC/RFC9110/sections/07_3_4_messages.md index 319ee4c90..12346e9b2 100644 --- a/notes/RFC/RFC9110/sections/07_3_4_messages.md +++ b/notes/RFC/RFC9110/sections/07_3_4_messages.md @@ -1,4 +1,4 @@ ---- +--- title: "3.4. Messages" rfc_number: 9110 rfc_section: "3.4" @@ -33,4 +33,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/08_3_5_user_agents.md b/notes/RFC/RFC9110/sections/08_3_5_user_agents.md index 0278f2aff..542262806 100644 --- a/notes/RFC/RFC9110/sections/08_3_5_user_agents.md +++ b/notes/RFC/RFC9110/sections/08_3_5_user_agents.md @@ -1,4 +1,4 @@ ---- +--- title: "3.5. User Agents" rfc_number: 9110 rfc_section: "3.5" @@ -43,4 +43,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/09_3_6_origin_server.md b/notes/RFC/RFC9110/sections/09_3_6_origin_server.md index 66b72e20b..c6c1277f6 100644 --- a/notes/RFC/RFC9110/sections/09_3_6_origin_server.md +++ b/notes/RFC/RFC9110/sections/09_3_6_origin_server.md @@ -1,4 +1,4 @@ ---- +--- title: "3.6. Origin Server" rfc_number: 9110 rfc_section: "3.6" @@ -36,4 +36,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/100_17_11_disclosure_of_fragment_after_redirects.md b/notes/RFC/RFC9110/sections/100_17_11_disclosure_of_fragment_after_redirects.md index d14006a33..3f23ef8f8 100644 --- a/notes/RFC/RFC9110/sections/100_17_11_disclosure_of_fragment_after_redirects.md +++ b/notes/RFC/RFC9110/sections/100_17_11_disclosure_of_fragment_after_redirects.md @@ -1,4 +1,4 @@ ---- +--- title: "17.11. Disclosure of Fragment after Redirects" rfc_number: 9110 rfc_section: "17.11" @@ -24,4 +24,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/101_17_12_disclosure_of_product_information.md b/notes/RFC/RFC9110/sections/101_17_12_disclosure_of_product_information.md index 3507a4bd7..bb1a6e595 100644 --- a/notes/RFC/RFC9110/sections/101_17_12_disclosure_of_product_information.md +++ b/notes/RFC/RFC9110/sections/101_17_12_disclosure_of_product_information.md @@ -1,4 +1,4 @@ ---- +--- title: "17.12. Disclosure of Product Information" rfc_number: 9110 rfc_section: "17.12" @@ -26,4 +26,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/102_17_13_browser_fingerprinting.md b/notes/RFC/RFC9110/sections/102_17_13_browser_fingerprinting.md index fc1797265..395e2e111 100644 --- a/notes/RFC/RFC9110/sections/102_17_13_browser_fingerprinting.md +++ b/notes/RFC/RFC9110/sections/102_17_13_browser_fingerprinting.md @@ -1,4 +1,4 @@ ---- +--- title: "17.13. Browser Fingerprinting" rfc_number: 9110 rfc_section: "17.13" @@ -61,4 +61,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/103_17_14_validator_retention.md b/notes/RFC/RFC9110/sections/103_17_14_validator_retention.md index 3a00907f0..a03bcf03d 100644 --- a/notes/RFC/RFC9110/sections/103_17_14_validator_retention.md +++ b/notes/RFC/RFC9110/sections/103_17_14_validator_retention.md @@ -1,4 +1,4 @@ ---- +--- title: "17.14. Validator Retention" rfc_number: 9110 rfc_section: "17.14" @@ -33,4 +33,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/104_17_15_denial-of-service_attacks_using_range.md b/notes/RFC/RFC9110/sections/104_17_15_denial-of-service_attacks_using_range.md index 9715004ab..486cd40cc 100644 --- a/notes/RFC/RFC9110/sections/104_17_15_denial-of-service_attacks_using_range.md +++ b/notes/RFC/RFC9110/sections/104_17_15_denial-of-service_attacks_using_range.md @@ -1,4 +1,4 @@ ---- +--- title: "17.15. Denial-of-Service Attacks Using Range" rfc_number: 9110 rfc_section: "17.15" @@ -24,4 +24,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/105_17_16_authentication_considerations.md b/notes/RFC/RFC9110/sections/105_17_16_authentication_considerations.md index 70618e4db..7f96b4d8a 100644 --- a/notes/RFC/RFC9110/sections/105_17_16_authentication_considerations.md +++ b/notes/RFC/RFC9110/sections/105_17_16_authentication_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "17.16. Authentication Considerations" rfc_number: 9110 rfc_section: "17.16" @@ -98,4 +98,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/106_18_iana_considerations.md b/notes/RFC/RFC9110/sections/106_18_iana_considerations.md index 142e1d73f..29d25d90f 100644 --- a/notes/RFC/RFC9110/sections/106_18_iana_considerations.md +++ b/notes/RFC/RFC9110/sections/106_18_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "18. IANA Considerations" rfc_number: 9110 rfc_section: "18" @@ -179,4 +179,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/107_1_the_applicable_protocol_field_has_been_omitted.md b/notes/RFC/RFC9110/sections/107_1_the_applicable_protocol_field_has_been_omitted.md index 216045a8d..db4f03525 100644 --- a/notes/RFC/RFC9110/sections/107_1_the_applicable_protocol_field_has_been_omitted.md +++ b/notes/RFC/RFC9110/sections/107_1_the_applicable_protocol_field_has_been_omitted.md @@ -1,4 +1,4 @@ ---- +--- title: "1. The 'Applicable Protocol' field has been omitted." rfc_number: 9110 rfc_section: "1" @@ -229,4 +229,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/108_19_1_normative_references.md b/notes/RFC/RFC9110/sections/108_19_1_normative_references.md index 81af6db9c..4e0e0fa3b 100644 --- a/notes/RFC/RFC9110/sections/108_19_1_normative_references.md +++ b/notes/RFC/RFC9110/sections/108_19_1_normative_references.md @@ -1,4 +1,4 @@ ---- +--- title: "19.1. Normative References" rfc_number: 9110 rfc_section: "19.1" @@ -112,4 +112,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/109_19_2_informative_references.md b/notes/RFC/RFC9110/sections/109_19_2_informative_references.md index 660f977e5..e218a8051 100644 --- a/notes/RFC/RFC9110/sections/109_19_2_informative_references.md +++ b/notes/RFC/RFC9110/sections/109_19_2_informative_references.md @@ -1,4 +1,4 @@ ---- +--- title: "19.2. Informative References" rfc_number: 9110 rfc_section: "19.2" @@ -303,4 +303,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/10_3_7_intermediaries.md b/notes/RFC/RFC9110/sections/10_3_7_intermediaries.md index ec8b8e312..ba40ca20c 100644 --- a/notes/RFC/RFC9110/sections/10_3_7_intermediaries.md +++ b/notes/RFC/RFC9110/sections/10_3_7_intermediaries.md @@ -1,4 +1,4 @@ ---- +--- title: "3.7. Intermediaries" rfc_number: 9110 rfc_section: "3.7" @@ -103,4 +103,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/110_appendix_a_collected_abnf.md b/notes/RFC/RFC9110/sections/110_appendix_a_collected_abnf.md index ec988fbb4..a64d867fc 100644 --- a/notes/RFC/RFC9110/sections/110_appendix_a_collected_abnf.md +++ b/notes/RFC/RFC9110/sections/110_appendix_a_collected_abnf.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Collected ABNF" rfc_number: 9110 rfc_section: "Appendix A" @@ -14,7 +14,6 @@ Appendix A. Collected ABNF In the collected ABNF below, list rules are expanded per Section 5.6.1. - ```abnf Accept = [ ( media-range [ weight ] ) *( OWS "," OWS ( media-range [ ``` @@ -125,7 +124,6 @@ Appendix A. Collected ABNF "," OWS ( received-protocol RWS received-by [ RWS comment ] ) ) ] - ```abnf WWW-Authenticate = [ challenge *( OWS "," OWS challenge ) ] @@ -234,7 +232,6 @@ Appendix A. Collected ABNF ) - ```abnf parameter = parameter-name "=" parameter-value parameter-name = token @@ -300,4 +297,3 @@ Appendix A. Collected ABNF --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/111_appendix_b_changes_from_previous_rfcs.md b/notes/RFC/RFC9110/sections/111_appendix_b_changes_from_previous_rfcs.md index 038f6fcc2..0d73e3c19 100644 --- a/notes/RFC/RFC9110/sections/111_appendix_b_changes_from_previous_rfcs.md +++ b/notes/RFC/RFC9110/sections/111_appendix_b_changes_from_previous_rfcs.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Changes from Previous RFCs" rfc_number: 9110 rfc_section: "Appendix B" @@ -204,4 +204,3 @@ B.9. Changes from RFC 7694 --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/11_3_8_caches.md b/notes/RFC/RFC9110/sections/11_3_8_caches.md index a010fd3ee..fc15e0637 100644 --- a/notes/RFC/RFC9110/sections/11_3_8_caches.md +++ b/notes/RFC/RFC9110/sections/11_3_8_caches.md @@ -1,4 +1,4 @@ ---- +--- title: "3.8. Caches" rfc_number: 9110 rfc_section: "3.8" @@ -48,4 +48,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/12_3_9_example_message_exchange.md b/notes/RFC/RFC9110/sections/12_3_9_example_message_exchange.md index ac0385804..390f86df3 100644 --- a/notes/RFC/RFC9110/sections/12_3_9_example_message_exchange.md +++ b/notes/RFC/RFC9110/sections/12_3_9_example_message_exchange.md @@ -1,4 +1,4 @@ ---- +--- title: "3.9. Example Message Exchange" rfc_number: 9110 rfc_section: "3.9" @@ -38,4 +38,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/13_4_1_uri_references.md b/notes/RFC/RFC9110/sections/13_4_1_uri_references.md index e95610ff0..e2482d1fd 100644 --- a/notes/RFC/RFC9110/sections/13_4_1_uri_references.md +++ b/notes/RFC/RFC9110/sections/13_4_1_uri_references.md @@ -1,4 +1,4 @@ ---- +--- title: "4.1. URI References" rfc_number: 9110 rfc_section: "4.1" @@ -29,7 +29,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte rule is defined for protocol elements that can contain a relative URI but not a fragment component. - ```abnf URI-reference = absolute-URI = @@ -45,7 +44,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte partial-URI = relative-part [ "?" query ] ``` - Each protocol element in HTTP that allows a URI reference will indicate in its ABNF production whether the element allows any form of reference (URI-reference), only a URI in absolute form (absolute- @@ -61,4 +59,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/14_4_2_http-related_uri_schemes.md b/notes/RFC/RFC9110/sections/14_4_2_http-related_uri_schemes.md index 6a62477c1..63e3b7fa2 100644 --- a/notes/RFC/RFC9110/sections/14_4_2_http-related_uri_schemes.md +++ b/notes/RFC/RFC9110/sections/14_4_2_http-related_uri_schemes.md @@ -1,4 +1,4 @@ ---- +--- title: "4.2. HTTP-Related URI Schemes" rfc_number: 9110 rfc_section: "4.2" @@ -40,12 +40,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte within the hierarchical namespace governed by a potential HTTP origin server listening for TCP ([TCP]) connections on a given port. - ```abnf http-URI = "http" "://" authority path-abempty [ "?" query ] ``` - The origin server for an "http" URI is identified by the authority component, which includes a host identifier ([URI], Section 3.2.2) and optional port number ([URI], Section 3.2.3). If the port @@ -73,12 +71,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte confidentiality and integrity protection that is acceptable to both client and server. - ```abnf https-URI = "https" "://" authority path-abempty [ "?" query ] ``` - The origin server for an "https" URI is identified by the authority component, which includes a host identifier ([URI], Section 3.2.2) and optional port number ([URI], Section 3.2.3). If the port @@ -188,4 +184,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/15_4_3_authoritative_access.md b/notes/RFC/RFC9110/sections/15_4_3_authoritative_access.md index 50447fa53..d3349885b 100644 --- a/notes/RFC/RFC9110/sections/15_4_3_authoritative_access.md +++ b/notes/RFC/RFC9110/sections/15_4_3_authoritative_access.md @@ -1,4 +1,4 @@ ---- +--- title: "4.3. Authoritative Access" rfc_number: 9110 rfc_section: "4.3" @@ -229,4 +229,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/16_5_1_field_names.md b/notes/RFC/RFC9110/sections/16_5_1_field_names.md index cd42e79fc..225fee1ad 100644 --- a/notes/RFC/RFC9110/sections/16_5_1_field_names.md +++ b/notes/RFC/RFC9110/sections/16_5_1_field_names.md @@ -1,4 +1,4 @@ ---- +--- title: "5.1. Field Names" rfc_number: 9110 rfc_section: "5.1" @@ -23,12 +23,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte is defined in Section 6.6.1 as containing the origination timestamp for the message in which it appears. - ```abnf field-name = token ``` - Field names are case-insensitive and ought to be registered within the "Hypertext Transfer Protocol (HTTP) Field Name Registry"; see Section 16.3.1. @@ -55,4 +53,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/17_5_2_field_lines_and_combined_field_value.md b/notes/RFC/RFC9110/sections/17_5_2_field_lines_and_combined_field_value.md index 8d9c2b28c..70c667b40 100644 --- a/notes/RFC/RFC9110/sections/17_5_2_field_lines_and_combined_field_value.md +++ b/notes/RFC/RFC9110/sections/17_5_2_field_lines_and_combined_field_value.md @@ -1,4 +1,4 @@ ---- +--- title: "5.2. Field Lines and Combined Field Value" rfc_number: 9110 rfc_section: "5.2" @@ -34,4 +34,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/18_5_3_field_order.md b/notes/RFC/RFC9110/sections/18_5_3_field_order.md index 8bd6c37ae..67ba833b6 100644 --- a/notes/RFC/RFC9110/sections/18_5_3_field_order.md +++ b/notes/RFC/RFC9110/sections/18_5_3_field_order.md @@ -1,4 +1,4 @@ ---- +--- title: "5.3. Field Order" rfc_number: 9110 rfc_section: "5.3" @@ -56,4 +56,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/19_5_4_field_limits.md b/notes/RFC/RFC9110/sections/19_5_4_field_limits.md index 2094aaffb..02942d737 100644 --- a/notes/RFC/RFC9110/sections/19_5_4_field_limits.md +++ b/notes/RFC/RFC9110/sections/19_5_4_field_limits.md @@ -1,4 +1,4 @@ ---- +--- title: "5.4. Field Limits" rfc_number: 9110 rfc_section: "5.4" @@ -30,4 +30,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/20_5_5_field_values.md b/notes/RFC/RFC9110/sections/20_5_5_field_values.md index d6338f07b..a50efc278 100644 --- a/notes/RFC/RFC9110/sections/20_5_5_field_values.md +++ b/notes/RFC/RFC9110/sections/20_5_5_field_values.md @@ -1,4 +1,4 @@ ---- +--- title: "5.5. Field Values" rfc_number: 9110 rfc_section: "5.5" @@ -15,7 +15,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte defined by the field's grammar. Each field's grammar is usually defined using ABNF ([RFC5234]). - ```abnf field-value = *field-content field-content = field-vchar @@ -24,7 +23,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte obs-text = %x80-FF ``` - A field value does not include leading or trailing whitespace. When a specific version of HTTP allows such whitespace to appear in a > **MUST**: message, a field parsing implementation MUST exclude such whitespace @@ -97,4 +95,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/21_5_6_common_rules_for_defining_field_values.md b/notes/RFC/RFC9110/sections/21_5_6_common_rules_for_defining_field_values.md index 1934e7b60..146cf45f9 100644 --- a/notes/RFC/RFC9110/sections/21_5_6_common_rules_for_defining_field_values.md +++ b/notes/RFC/RFC9110/sections/21_5_6_common_rules_for_defining_field_values.md @@ -1,4 +1,4 @@ ---- +--- title: "5.6. Common Rules for Defining Field Values" rfc_number: 9110 rfc_section: "5.6" @@ -59,13 +59,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte For example, given these ABNF productions: - ```abnf example-list = 1#example-list-elmt example-list-elmt = token ; see Section 5.6.2 ``` - Then the following are valid values for example-list (not including the double quotes, which are present for delimitation only): @@ -85,7 +83,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Tokens are short textual identifiers that do not include whitespace or delimiters. - ```abnf token = 1*tchar @@ -95,7 +92,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ; any VCHAR, except delimiters ``` - Many HTTP field values are defined using common syntax components, separated by whitespace or specific delimiting characters. Delimiters are chosen from the set of US-ASCII visual characters not @@ -130,7 +126,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **MAY**: BWS has no semantics. Any content known to be defined as BWS MAY be removed before interpreting it or forwarding the message downstream. - ```abnf OWS = *( SP / HTAB ) ; optional whitespace @@ -140,30 +135,25 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ; "bad" whitespace ``` - ### 5.6.4 Quoted Strings A string of text is parsed as a single value if it is quoted using double-quote marks. - ```abnf quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text ``` - The backslash octet ("\") can be used as a single-octet quoting mechanism within quoted-string and comment constructs. Recipients > **MUST**: that process the value of a quoted-string MUST handle a quoted-pair as if it were replaced by the octet following the backslash. - ```abnf quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) ``` - > **SHOULD NOT**: A sender SHOULD NOT generate a quoted-pair in a quoted-string except where necessary to quote DQUOTE and backslash octets occurring within > **SHOULD NOT**: that string. A sender SHOULD NOT generate a quoted-pair in a comment @@ -176,13 +166,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte comment text with parentheses. Comments are only allowed in fields containing "comment" as part of their field value definition. - ```abnf comment = "(" *( ctext / quoted-pair / comment ) ")" ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text ``` - ### 5.6.6 Parameters Parameters are instances of name/value pairs; they are often used in @@ -190,7 +178,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte to an item. Each parameter is usually delimited by an immediately preceding semicolon. - ```abnf parameters = *( OWS ";" OWS [ parameter ] ) parameter = parameter-name "=" parameter-value @@ -198,7 +185,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte parameter-value = ( token / quoted-string ) ``` - Parameter names are case-insensitive. Parameter values might or might not be case-sensitive, depending on the semantics of the parameter name. Examples of parameters and some equivalent forms can @@ -220,12 +206,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte a fixed-length and single-zone subset of the date and time specification used by the Internet Message Format [RFC5322]. - ```abnf HTTP-date = IMF-fixdate / obs-date ``` - An example of the preferred format is Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate @@ -253,7 +237,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Preferred format: - ```abnf IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT ``` @@ -261,7 +244,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ; fixed length/zone/capitalization subset of the format ; see Section 3.3 of [RFC5322] - ```abnf day-name = %s"Mon" / %s"Tue" / %s"Wed" / %s"Thu" / %s"Fri" / %s"Sat" / %s"Sun" @@ -285,10 +267,8 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte second = 2DIGIT ``` - Obsolete formats: - ```abnf obs-date = rfc850-date / asctime-date @@ -305,7 +285,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ; e.g., Jun 2 ``` - HTTP-date is case sensitive. Note that Section 4.2 of [CACHING] relaxes this for cache recipients. @@ -333,4 +312,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/22_6_1_framing_and_completeness.md b/notes/RFC/RFC9110/sections/22_6_1_framing_and_completeness.md index 5f597a6d2..f771aff21 100644 --- a/notes/RFC/RFC9110/sections/22_6_1_framing_and_completeness.md +++ b/notes/RFC/RFC9110/sections/22_6_1_framing_and_completeness.md @@ -1,4 +1,4 @@ ---- +--- title: 6.1. Framing and Completeness rfc_number: 9110 rfc_section: '6.1' @@ -96,26 +96,3 @@ tags: close is considered complete even though it might be indistinguishable from an incomplete response, unless a transport- level error indicates that it is not complete. - - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`Http11ResponseDecoder.cs`** — Detects message completeness via Content-Length or chunked transfer coding; handles connection-close framing for HTTP/1.0 -- **`Http2FrameDecoder.cs`** — Uses END_STREAM flag for message completeness in HTTP/2 -- **`Http3FrameDecoder.cs`** — Uses FIN bit on QUIC streams for HTTP/3 message completeness -- **`MessageCompleteness.cs`** — Shared abstraction tracking whether headers, content, and trailers are complete - -### Test References -- `TurboHTTP.Tests/RFC9110/22_FramingCompletenessTests.cs` — Message completeness detection across protocol versions - -### Known Gaps -- None - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/23_6_2_control_data.md b/notes/RFC/RFC9110/sections/23_6_2_control_data.md index e07f53d79..c6d2839cd 100644 --- a/notes/RFC/RFC9110/sections/23_6_2_control_data.md +++ b/notes/RFC/RFC9110/sections/23_6_2_control_data.md @@ -1,4 +1,4 @@ ---- +--- title: 6.2. Control Data rfc_number: 9110 rfc_section: '6.2' @@ -70,26 +70,3 @@ tags: support for that higher version, is sufficiently backwards-compatible to be safely processed by any implementation of the same major version. - - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`HttpRequestEncoder.cs`** — Sets protocol version in request control data; sends highest conformant version per §6.2 -- **`Http11RequestEncoder.cs`** — Encodes request-line with method, request-target, and HTTP/1.1 version -- **`Http2RequestEncoder.cs`** — Maps control data to pseudo-header fields (`:method`, `:path`, `:scheme`, `:authority`) -- **`HttpResponseDecoder.cs`** — Parses status code and reason phrase from response control data - -### Test References -- `TurboHTTP.Tests/RFC9110/23_ControlDataTests.cs` — Version negotiation, pseudo-header mapping - -### Known Gaps -- ⚠️ Version downgrade — Client does not automatically retry with lower HTTP version if server indicates incompatibility - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/24_6_3_header_fields.md b/notes/RFC/RFC9110/sections/24_6_3_header_fields.md index babbc582e..270bb8d7a 100644 --- a/notes/RFC/RFC9110/sections/24_6_3_header_fields.md +++ b/notes/RFC/RFC9110/sections/24_6_3_header_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "6.3. Header Fields" rfc_number: 9110 rfc_section: "6.3" @@ -25,4 +25,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/25_6_4_content.md b/notes/RFC/RFC9110/sections/25_6_4_content.md index 9f084a7fe..6e0339b6c 100644 --- a/notes/RFC/RFC9110/sections/25_6_4_content.md +++ b/notes/RFC/RFC9110/sections/25_6_4_content.md @@ -1,4 +1,4 @@ ---- +--- title: 6.4. Content rfc_number: 9110 rfc_section: '6.4' @@ -140,26 +140,3 @@ tags: 7. Otherwise, the content is unidentified by HTTP, but a more specific identifier might be supplied within the content itself. - - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`HttpResponseDecoder.cs`** — Extracts content from message framing; handles zero-length content for 204/304 responses per §6.4.1 -- **`ContentDecodingStage.cs`** — Decodes content after extracting from framing layer; supports streaming content delivery -- **`Http11ResponseDecoder.cs`** — Handles chunked transfer coding extraction to produce raw content stream -- **`ContentIdentification.cs`** — Applies §6.4.2 rules for identifying content via Content-Location and request method - -### Test References -- `TurboHTTP.Tests/RFC9110/25_ContentTests.cs` — Content semantics, zero-length bodies, HEAD response handling - -### Known Gaps -- None - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/26_6_5_trailer_fields.md b/notes/RFC/RFC9110/sections/26_6_5_trailer_fields.md index 473c46356..4e96eab1d 100644 --- a/notes/RFC/RFC9110/sections/26_6_5_trailer_fields.md +++ b/notes/RFC/RFC9110/sections/26_6_5_trailer_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "6.5. Trailer Fields" rfc_number: 9110 rfc_section: "6.5" @@ -89,4 +89,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/27_6_6_message_metadata.md b/notes/RFC/RFC9110/sections/27_6_6_message_metadata.md index 0b839a3fa..f8c120a50 100644 --- a/notes/RFC/RFC9110/sections/27_6_6_message_metadata.md +++ b/notes/RFC/RFC9110/sections/27_6_6_message_metadata.md @@ -1,4 +1,4 @@ ---- +--- title: "6.6. Message Metadata" rfc_number: 9110 rfc_section: "6.6" @@ -22,12 +22,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Date Field (orig-date) defined in Section 3.6.1 of [RFC5322]. The field value is an HTTP-date, as defined in Section 5.6.7. - ```abnf Date = HTTP-date ``` - An example is Date: Tue, 15 Nov 1994 08:12:31 GMT @@ -71,12 +69,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte This allows a recipient to prepare for receipt of the indicated metadata before it starts processing the content. - ```abnf Trailer = #field-name ``` - For example, a sender might indicate that a signature will be computed as the content is being streamed and provide the final signature as a trailer field. This allows a recipient to perform the @@ -94,4 +90,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/28_7_1_determining_the_target_resource.md b/notes/RFC/RFC9110/sections/28_7_1_determining_the_target_resource.md index 388edf757..289bd91ef 100644 --- a/notes/RFC/RFC9110/sections/28_7_1_determining_the_target_resource.md +++ b/notes/RFC/RFC9110/sections/28_7_1_determining_the_target_resource.md @@ -1,4 +1,4 @@ ---- +--- title: "7.1. Determining the Target Resource" rfc_number: 9110 rfc_section: "7.1" @@ -61,4 +61,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/29_7_2_host_and_authority.md b/notes/RFC/RFC9110/sections/29_7_2_host_and_authority.md index a3e8123c1..bd3d26206 100644 --- a/notes/RFC/RFC9110/sections/29_7_2_host_and_authority.md +++ b/notes/RFC/RFC9110/sections/29_7_2_host_and_authority.md @@ -1,4 +1,4 @@ ---- +--- title: "7.2. Host and :authority" rfc_number: 9110 rfc_section: "7.2" @@ -20,12 +20,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte some cases, supplanted by the ":authority" pseudo-header field of a request's control data. - ```abnf Host = uri-host [ ":" port ] ; Section 4 ``` - The target URI's authority information is critical for handling a > **MUST**: request. A user agent MUST generate a Host header field in a request unless it sends that information as an ":authority" pseudo-header @@ -49,4 +47,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/30_7_3_routing_inbound_requests.md b/notes/RFC/RFC9110/sections/30_7_3_routing_inbound_requests.md index de0d343e0..f8aeda9b2 100644 --- a/notes/RFC/RFC9110/sections/30_7_3_routing_inbound_requests.md +++ b/notes/RFC/RFC9110/sections/30_7_3_routing_inbound_requests.md @@ -1,4 +1,4 @@ ---- +--- title: "7.3. Routing Inbound Requests" rfc_number: 9110 rfc_section: "7.3" @@ -54,4 +54,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/31_7_4_rejecting_misdirected_requests.md b/notes/RFC/RFC9110/sections/31_7_4_rejecting_misdirected_requests.md index 106203bbe..240e22c0b 100644 --- a/notes/RFC/RFC9110/sections/31_7_4_rejecting_misdirected_requests.md +++ b/notes/RFC/RFC9110/sections/31_7_4_rejecting_misdirected_requests.md @@ -1,4 +1,4 @@ ---- +--- title: "7.4. Rejecting Misdirected Requests" rfc_number: 9110 rfc_section: "7.4" @@ -43,4 +43,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/32_7_5_response_correlation.md b/notes/RFC/RFC9110/sections/32_7_5_response_correlation.md index b2b52ff66..3bd8fab91 100644 --- a/notes/RFC/RFC9110/sections/32_7_5_response_correlation.md +++ b/notes/RFC/RFC9110/sections/32_7_5_response_correlation.md @@ -1,4 +1,4 @@ ---- +--- title: "7.5. Response Correlation" rfc_number: 9110 rfc_section: "7.5" @@ -31,4 +31,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/33_7_6_message_forwarding.md b/notes/RFC/RFC9110/sections/33_7_6_message_forwarding.md index e48fb2d26..d046a5efd 100644 --- a/notes/RFC/RFC9110/sections/33_7_6_message_forwarding.md +++ b/notes/RFC/RFC9110/sections/33_7_6_message_forwarding.md @@ -1,4 +1,4 @@ ---- +--- title: "7.6. Message Forwarding" rfc_number: 9110 rfc_section: "7.6" @@ -46,13 +46,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte The "Connection" header field allows the sender to list desired control options for the current connection. - ```abnf Connection = #connection-option connection-option = token ``` - Connection options are case-insensitive. When a field aside from Connection is used to supply control @@ -118,12 +116,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte can be useful when the client is attempting to trace a request that appears to be failing or looping mid-chain. - ```abnf Max-Forwards = 1*DIGIT ``` - The Max-Forwards value is a decimal integer indicating the remaining number of times this request message can be forwarded. @@ -150,7 +146,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte request loops, and identifying the protocol capabilities of senders along the request/response chain. - ```abnf Via = #( received-protocol RWS received-by [ RWS comment ] ) @@ -160,7 +155,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte pseudonym = token ``` - Each member of the Via field value represents a proxy or gateway that has forwarded the message. Each intermediary appends its own information about how the message was received, such that the end @@ -226,4 +220,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/34_7_7_message_transformations.md b/notes/RFC/RFC9110/sections/34_7_7_message_transformations.md index 76bb7346c..50b1d216c 100644 --- a/notes/RFC/RFC9110/sections/34_7_7_message_transformations.md +++ b/notes/RFC/RFC9110/sections/34_7_7_message_transformations.md @@ -1,4 +1,4 @@ ---- +--- title: "7.7. Message Transformations" rfc_number: 9110 rfc_section: "7.7" @@ -65,4 +65,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/35_7_8_upgrade.md b/notes/RFC/RFC9110/sections/35_7_8_upgrade.md index 45e1a2810..aa034f569 100644 --- a/notes/RFC/RFC9110/sections/35_7_8_upgrade.md +++ b/notes/RFC/RFC9110/sections/35_7_8_upgrade.md @@ -1,4 +1,4 @@ ---- +--- title: "7.8. Upgrade" rfc_number: 9110 rfc_section: "7.8" @@ -23,7 +23,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte that connection. Upgrade cannot be used to insist on a protocol change. - ```abnf Upgrade = #protocol @@ -32,7 +31,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte protocol-version = token ``` - Although protocol names are registered with a preferred case, > **SHOULD**: recipients SHOULD use case-insensitive comparison when matching each protocol-name to supported protocols. @@ -121,4 +119,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/36_8_1_representation_data.md b/notes/RFC/RFC9110/sections/36_8_1_representation_data.md index cc8bc2c47..69b92e10a 100644 --- a/notes/RFC/RFC9110/sections/36_8_1_representation_data.md +++ b/notes/RFC/RFC9110/sections/36_8_1_representation_data.md @@ -1,4 +1,4 @@ ---- +--- title: "8.1. Representation Data" rfc_number: 9110 rfc_section: "8.1" @@ -26,4 +26,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/37_8_2_representation_metadata.md b/notes/RFC/RFC9110/sections/37_8_2_representation_metadata.md index 5ca21764c..60701231e 100644 --- a/notes/RFC/RFC9110/sections/37_8_2_representation_metadata.md +++ b/notes/RFC/RFC9110/sections/37_8_2_representation_metadata.md @@ -1,4 +1,4 @@ ---- +--- title: "8.2. Representation Metadata" rfc_number: 9110 rfc_section: "8.2" @@ -20,4 +20,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/38_8_3_content-type.md b/notes/RFC/RFC9110/sections/38_8_3_content-type.md index 3e253ecf4..03a77d072 100644 --- a/notes/RFC/RFC9110/sections/38_8_3_content-type.md +++ b/notes/RFC/RFC9110/sections/38_8_3_content-type.md @@ -1,4 +1,4 @@ ---- +--- title: "8.3. Content-Type" rfc_number: 9110 rfc_section: "8.3" @@ -19,12 +19,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte within the scope of the received message semantics, after any content codings indicated by Content-Encoding are decoded. - ```abnf Content-Type = media-type ``` - Media types are defined in Section 8.3.1. An example of the field is Content-Type: text/html; charset=ISO-8859-4 @@ -65,14 +63,12 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte a data format and various processing models: how to process that data in accordance with the message context. - ```abnf media-type = type "/" subtype parameters type = token subtype = token ``` - The type and subtype tokens are case-insensitive. > **MAY**: The type/subtype MAY be followed by semicolon-delimited parameters @@ -134,4 +130,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/39_8_4_content-encoding.md b/notes/RFC/RFC9110/sections/39_8_4_content-encoding.md index 4ad44425a..84e3aa939 100644 --- a/notes/RFC/RFC9110/sections/39_8_4_content-encoding.md +++ b/notes/RFC/RFC9110/sections/39_8_4_content-encoding.md @@ -1,4 +1,4 @@ ---- +--- title: "8.4. Content-Encoding" rfc_number: 9110 rfc_section: "8.4" @@ -108,25 +108,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Check (CRC) that is commonly produced by the gzip file compression > **SHOULD**: program [RFC1952]. A recipient SHOULD consider "x-gzip" to be equivalent to "gzip". - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`DecompressionStage.cs`** — Decodes gzip, deflate, and br (Brotli) content encodings; processes Content-Encoding header to determine decoding chain order -- **`ContentEncodingHandler.cs`** — Parses Content-Encoding header; applies decodings in reverse order per §8.4 -- **`AcceptEncodingBuilder.cs`** — Generates Accept-Encoding request header advertising supported codings (gzip, deflate, br) - -### Test References -- `TurboHTTP.Tests/RFC9110/39_ContentEncodingTests.cs` — gzip/deflate/br decoding, multi-layer encoding, x-gzip equivalence - -### Known Gaps -- ❌ Compress (LZW) — Not supported; x-compress/compress coding not implemented -- ⚠️ Identity coding — Correctly excluded from Content-Encoding but not explicitly validated on receipt - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/40_8_5_content-language.md b/notes/RFC/RFC9110/sections/40_8_5_content-language.md index 1da5398b7..efe0ac2ce 100644 --- a/notes/RFC/RFC9110/sections/40_8_5_content-language.md +++ b/notes/RFC/RFC9110/sections/40_8_5_content-language.md @@ -1,4 +1,4 @@ ---- +--- title: "8.5. Content-Language" rfc_number: 9110 rfc_section: "8.5" @@ -16,12 +16,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte might not be equivalent to all the languages used within the representation. - ```abnf Content-Language = #language-tag ``` - Language tags are defined in Section 8.5.1. The primary purpose of Content-Language is to allow a user to identify and differentiate representations according to the users' own preferred language. @@ -64,12 +62,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte language-range production defined in Section 12.5.4, whereas Content-Language uses the language-tag production defined below. - ```abnf language-tag = ``` - A language tag is a sequence of one or more case-insensitive subtags, each separated by a hyphen character ("-", %x2D). In most cases, a language tag consists of a primary language subtag that identifies a @@ -85,4 +81,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/41_8_6_content-length.md b/notes/RFC/RFC9110/sections/41_8_6_content-length.md index 199721079..8dc247b10 100644 --- a/notes/RFC/RFC9110/sections/41_8_6_content-length.md +++ b/notes/RFC/RFC9110/sections/41_8_6_content-length.md @@ -1,4 +1,4 @@ ---- +--- title: "8.6. Content-Length" rfc_number: 9110 rfc_section: "8.6" @@ -20,12 +20,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte current length, which can be used by recipients to estimate transfer time or to compare with previously stored representations. - ```abnf Content-Length = 1*DIGIT ``` - An example is Content-Length: 3495 @@ -90,4 +88,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/42_8_7_content-location.md b/notes/RFC/RFC9110/sections/42_8_7_content-location.md index b85fe9f50..5e077df2e 100644 --- a/notes/RFC/RFC9110/sections/42_8_7_content-location.md +++ b/notes/RFC/RFC9110/sections/42_8_7_content-location.md @@ -1,4 +1,4 @@ ---- +--- title: "8.7. Content-Location" rfc_number: 9110 rfc_section: "8.7" @@ -18,12 +18,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte message's generation, then a 200 (OK) response would contain the same representation that is enclosed as content in this message. - ```abnf Content-Location = absolute-URI / partial-URI ``` - The field value is either an absolute-URI or a partial-URI. In the latter case (Section 4), the referenced URI is relative to the target URI ([URI], Section 5). @@ -101,4 +99,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/43_8_8_validator_fields.md b/notes/RFC/RFC9110/sections/43_8_8_validator_fields.md index 49ba98372..71b89d515 100644 --- a/notes/RFC/RFC9110/sections/43_8_8_validator_fields.md +++ b/notes/RFC/RFC9110/sections/43_8_8_validator_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "8.8. Validator Fields" rfc_number: 9110 rfc_section: "8.8" @@ -134,12 +134,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte selected representation was last modified, as determined at the conclusion of handling the request. - ```abnf Last-Modified = HTTP-date ``` - An example of its use is Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT @@ -233,7 +231,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte or both. An entity tag consists of an opaque quoted string, possibly prefixed by a weakness indicator. - ```abnf ETag = entity-tag @@ -244,7 +241,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ; VCHAR except double quotes, plus obs-text ``` - | *Note:* Previously, opaque-tag was defined to be a quoted- | string ([RFC2616], Section 3.11); thus, some recipients might | perform backslash unescaping. Servers therefore ought to avoid @@ -383,4 +379,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/44_9_1_overview.md b/notes/RFC/RFC9110/sections/44_9_1_overview.md index 4b7bfc942..9072df5ca 100644 --- a/notes/RFC/RFC9110/sections/44_9_1_overview.md +++ b/notes/RFC/RFC9110/sections/44_9_1_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "9.1. Overview" rfc_number: 9110 rfc_section: "9.1" @@ -29,12 +29,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte target resource in much the same way that a remote method invocation can be sent to an identified object. - ```abnf method = token ``` - The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US- @@ -100,4 +98,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/45_9_2_common_method_properties.md b/notes/RFC/RFC9110/sections/45_9_2_common_method_properties.md index 76ef12a4a..5b503371e 100644 --- a/notes/RFC/RFC9110/sections/45_9_2_common_method_properties.md +++ b/notes/RFC/RFC9110/sections/45_9_2_common_method_properties.md @@ -1,4 +1,4 @@ ---- +--- title: "9.2. Common Method Properties" rfc_number: 9110 rfc_section: "9.2" @@ -118,4 +118,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/46_9_3_method_definitions.md b/notes/RFC/RFC9110/sections/46_9_3_method_definitions.md index ab3fd0fd0..d66f1e14e 100644 --- a/notes/RFC/RFC9110/sections/46_9_3_method_definitions.md +++ b/notes/RFC/RFC9110/sections/46_9_3_method_definitions.md @@ -1,4 +1,4 @@ ---- +--- title: "9.3. Method Definitions" rfc_number: 9110 rfc_section: "9.3" @@ -498,26 +498,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **MUST NOT**: A client MUST NOT send content in a TRACE request. Responses to the TRACE method are not cacheable. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes -- **`HttpRequestBuilder.cs`** — Supports all standard methods: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE -- **`RedirectStage.cs`** — Implements redirect method semantics: POST→GET for 301/302/303, method-preserving for 307/308 -- **`ConnectHandler.cs`** — CONNECT tunnel establishment through proxies per §9.3.6 -- **`HttpMethodProperties.cs`** — Safe/idempotent/cacheable method property lookup per §9.2 - -### Test References -- `TurboHTTP.Tests/RFC9110/46_MethodDefinitionTests.cs` — Method encoding, redirect method changes, safe/idempotent classification - -### Known Gaps -- ⚠️ TRACE — Not actively tested; client sends TRACE but response body parsing as message/http not implemented -- ⚠️ OPTIONS * — Server-wide OPTIONS with asterisk request-target not explicitly supported - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/47_10_1_request_context_fields.md b/notes/RFC/RFC9110/sections/47_10_1_request_context_fields.md index 33b51a52e..38b08b94f 100644 --- a/notes/RFC/RFC9110/sections/47_10_1_request_context_fields.md +++ b/notes/RFC/RFC9110/sections/47_10_1_request_context_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "10.1. Request Context Fields" rfc_number: 9110 rfc_section: "10.1" @@ -23,13 +23,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte behaviors (expectations) that need to be supported by the server in order to properly handle this request. - ```abnf Expect = #expectation expectation = token [ "=" ( token / quoted-string ) parameters ] ``` - The Expect field value is case-insensitive. The only expectation defined by this specification is "100-continue" @@ -144,14 +142,12 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte to be machine-usable, as defined by "mailbox" in Section 3.4 of [RFC5322]: - ```abnf From = mailbox mailbox = ``` - An example is: From: spider-admin@example.org @@ -179,12 +175,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **MUST NOT**: agent MUST NOT include the fragment and userinfo components of the URI reference [URI], if any, when generating the Referer field value. - ```abnf Referer = absolute-URI / partial-URI ``` - The field value is either an absolute-URI or a partial-URI. In the latter case (Section 4), the referenced URI is relative to the target URI ([URI], Section 5). @@ -258,7 +252,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte transfer coding (Section 12.4.2) and optional parameters for that transfer coding. - ```abnf TE = #t-codings t-codings = "trailers" / ( transfer-coding [ weight ] ) @@ -266,7 +259,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte transfer-parameter = token BWS "=" BWS ( token / quoted-string ) ``` - > **MUST**: A sender of TE MUST also send a "TE" connection option within the Connection header field (Section 7.6.1) to inform intermediaries not to forward this field. @@ -281,12 +273,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **SHOULD**: use. A user agent SHOULD send a User-Agent header field in each request unless specifically configured not to do so. - ```abnf User-Agent = product *( RWS ( product / comment ) ) ``` - The User-Agent field value consists of one or more product identifiers, each followed by zero or more comments (Section 5.6.5), which together identify the user agent software and its significant @@ -295,13 +285,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte software. Each product identifier consists of a name and optional version. - ```abnf product = token ["/" product-version] product-version = token ``` - > **SHOULD**: A sender SHOULD limit generated product identifiers to what is necessary to identify the product; a sender MUST NOT generate advertising or other nonessential information within the product @@ -330,4 +318,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/48_10_2_response_context_fields.md b/notes/RFC/RFC9110/sections/48_10_2_response_context_fields.md index b0162ae87..28e5fc2fc 100644 --- a/notes/RFC/RFC9110/sections/48_10_2_response_context_fields.md +++ b/notes/RFC/RFC9110/sections/48_10_2_response_context_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "10.2. Response Context Fields" rfc_number: 9110 rfc_section: "10.2" @@ -23,12 +23,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte strictly to inform the recipient of valid request methods associated with the resource. - ```abnf Allow = #method ``` - Example of use: Allow: GET, HEAD, PUT @@ -51,12 +49,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte relationship is defined by the combination of request method and status code semantics. - ```abnf Location = URI-reference ``` - The field value consists of a single URI-reference. When it has the form of a relative reference ([URI], Section 4.2), the final value is computed by resolving it against the target URI ([URI], Section 5). @@ -126,21 +122,17 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte The Retry-After field value can be either an HTTP-date or a number of seconds to delay after receiving the response. - ```abnf Retry-After = HTTP-date / delay-seconds ``` - A delay-seconds value is a non-negative decimal integer, representing time in seconds. - ```abnf delay-seconds = 1*DIGIT ``` - Two examples of its use are Retry-After: Fri, 31 Dec 1999 23:59:59 GMT @@ -158,12 +150,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **MAY**: system use. An origin server MAY generate a Server header field in its responses. - ```abnf Server = product *( RWS ( product / comment ) ) ``` - The Server header field value consists of one or more product identifiers, each followed by zero or more comments (Section 5.6.5), which together identify the origin server software and its @@ -185,4 +175,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/49_11_1_authentication_scheme.md b/notes/RFC/RFC9110/sections/49_11_1_authentication_scheme.md index dfcecf31c..4ac0f5703 100644 --- a/notes/RFC/RFC9110/sections/49_11_1_authentication_scheme.md +++ b/notes/RFC/RFC9110/sections/49_11_1_authentication_scheme.md @@ -1,4 +1,4 @@ ---- +--- title: "11.1. Authentication Scheme" rfc_number: 9110 rfc_section: "11.1" @@ -20,12 +20,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte It uses a case-insensitive token to identify the authentication scheme: - ```abnf auth-scheme = token ``` - Aside from the general framework, this document does not specify any authentication schemes. New and existing authentication schemes are specified independently and ought to be registered within the @@ -35,4 +33,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/50_11_2_authentication_parameters.md b/notes/RFC/RFC9110/sections/50_11_2_authentication_parameters.md index d0df3e3a5..f3a7e6208 100644 --- a/notes/RFC/RFC9110/sections/50_11_2_authentication_parameters.md +++ b/notes/RFC/RFC9110/sections/50_11_2_authentication_parameters.md @@ -1,4 +1,4 @@ ---- +--- title: "11.2. Authentication Parameters" rfc_number: 9110 rfc_section: "11.2" @@ -16,13 +16,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte comma-separated list of parameters or a single sequence of characters capable of holding base64-encoded information. - ```abnf token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" ``` - The token68 syntax allows the 66 unreserved URI characters ([URI]), plus a few others, so that it can hold a base64, base64url (URL and filename safe alphabet), base32, or base16 (hex) encoding, with or @@ -32,12 +30,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte > **MUST**: is matched case-insensitively and each parameter name MUST only occur once per challenge. - ```abnf auth-param = token BWS "=" BWS ( token / quoted-string ) ``` - Parameter values can be expressed either as "token" or as "quoted- string" (Section 5.6). Authentication scheme definitions need to accept both notations, both for senders and recipients, to allow @@ -51,4 +47,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/51_11_3_challenge_and_response.md b/notes/RFC/RFC9110/sections/51_11_3_challenge_and_response.md index d6f7b2e52..bcc98bed0 100644 --- a/notes/RFC/RFC9110/sections/51_11_3_challenge_and_response.md +++ b/notes/RFC/RFC9110/sections/51_11_3_challenge_and_response.md @@ -1,4 +1,4 @@ ---- +--- title: "11.3. Challenge and Response" rfc_number: 9110 rfc_section: "11.3" @@ -21,12 +21,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Proxy-Authenticate header field containing at least one challenge applicable to the proxy for the requested resource. - ```abnf challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ] ``` - | *Note:* Many clients fail to parse a challenge that contains an | unknown scheme. A workaround for this problem is to list well- | supported schemes (such as "basic") first. @@ -43,4 +41,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/52_11_4_credentials.md b/notes/RFC/RFC9110/sections/52_11_4_credentials.md index e3efa33d2..89315880b 100644 --- a/notes/RFC/RFC9110/sections/52_11_4_credentials.md +++ b/notes/RFC/RFC9110/sections/52_11_4_credentials.md @@ -1,4 +1,4 @@ ---- +--- title: "11.4. Credentials" rfc_number: 9110 rfc_section: "11.4" @@ -22,12 +22,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte considerations regarding the confidentiality of the underlying connection, as described in Section 17.16.1. - ```abnf credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ] ``` - Upon receipt of a request for a protected resource that omits credentials, contains invalid credentials (e.g., a bad password) or partial credentials (e.g., when the authentication scheme requires @@ -59,4 +57,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/53_11_5_establishing_a_protection_space_realm.md b/notes/RFC/RFC9110/sections/53_11_5_establishing_a_protection_space_realm.md index 5708b0a98..a48f2f7bd 100644 --- a/notes/RFC/RFC9110/sections/53_11_5_establishing_a_protection_space_realm.md +++ b/notes/RFC/RFC9110/sections/53_11_5_establishing_a_protection_space_realm.md @@ -1,4 +1,4 @@ ---- +--- title: "11.5. Establishing a Protection Space (Realm)" rfc_number: 9110 rfc_section: "11.5" @@ -46,4 +46,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/54_11_6_authenticating_users_to_origin_servers.md b/notes/RFC/RFC9110/sections/54_11_6_authenticating_users_to_origin_servers.md index 42392e935..3eb0fa671 100644 --- a/notes/RFC/RFC9110/sections/54_11_6_authenticating_users_to_origin_servers.md +++ b/notes/RFC/RFC9110/sections/54_11_6_authenticating_users_to_origin_servers.md @@ -1,4 +1,4 @@ ---- +--- title: "11.6. Authenticating Users to Origin Servers" rfc_number: 9110 rfc_section: "11.6" @@ -17,12 +17,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte authentication scheme(s) and parameters applicable to the target resource. - ```abnf WWW-Authenticate = #challenge ``` - > **MUST**: A server generating a 401 (Unauthorized) response MUST send a WWW- Authenticate header field containing at least one challenge. A > **MAY**: server MAY generate a WWW-Authenticate header field in other response @@ -67,12 +65,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte credentials containing the authentication information of the user agent for the realm of the resource being requested. - ```abnf Authorization = credentials ``` - If a request is authenticated and a realm specified, the same credentials are presumed to be valid for all other requests within this realm (assuming that the authentication scheme itself does not @@ -99,12 +95,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte "Digest" Authentication Scheme, for instance, defines multiple parameters in Section 3.5 of [RFC7616]. - ```abnf Authentication-Info = #auth-param ``` - The Authentication-Info field can be used in any HTTP response, independently of request method and status code. Its semantics are defined by the authentication scheme indicated by the Authorization @@ -118,4 +112,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/55_11_7_authenticating_clients_to_proxies.md b/notes/RFC/RFC9110/sections/55_11_7_authenticating_clients_to_proxies.md index ef7df78ec..00cf340b9 100644 --- a/notes/RFC/RFC9110/sections/55_11_7_authenticating_clients_to_proxies.md +++ b/notes/RFC/RFC9110/sections/55_11_7_authenticating_clients_to_proxies.md @@ -1,4 +1,4 @@ ---- +--- title: "11.7. Authenticating Clients to Proxies" rfc_number: 9110 rfc_section: "11.7" @@ -19,12 +19,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte one Proxy-Authenticate header field in each 407 (Proxy Authentication Required) response that it generates. - ```abnf Proxy-Authenticate = #challenge ``` - Unlike WWW-Authenticate, the Proxy-Authenticate header field applies only to the next outbound client on the response chain. This is because only the client that chose a given proxy is likely to have @@ -47,12 +45,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte information of the client for the proxy and/or realm of the resource being requested. - ```abnf Proxy-Authorization = credentials ``` - Unlike Authorization, the Proxy-Authorization header field applies only to the next inbound proxy that demanded authentication using the Proxy-Authenticate header field. When multiple proxies are used in a @@ -70,12 +66,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte authentication scheme indicated by the Proxy-Authorization header field (Section 11.7.2) of the corresponding request: - ```abnf Proxy-Authentication-Info = #auth-param ``` - However, unlike Authentication-Info, the Proxy-Authentication-Info header field applies only to the next outbound client on the response chain. This is because only the client that chose a given proxy is @@ -93,4 +87,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/56_12_1_proactive_negotiation.md b/notes/RFC/RFC9110/sections/56_12_1_proactive_negotiation.md index 29dae3893..3adede078 100644 --- a/notes/RFC/RFC9110/sections/56_12_1_proactive_negotiation.md +++ b/notes/RFC/RFC9110/sections/56_12_1_proactive_negotiation.md @@ -1,4 +1,4 @@ ---- +--- title: "12.1. Proactive Negotiation" rfc_number: 9110 rfc_section: "12.1" @@ -106,4 +106,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/57_12_2_reactive_negotiation.md b/notes/RFC/RFC9110/sections/57_12_2_reactive_negotiation.md index d3b5c572f..df65c4bf1 100644 --- a/notes/RFC/RFC9110/sections/57_12_2_reactive_negotiation.md +++ b/notes/RFC/RFC9110/sections/57_12_2_reactive_negotiation.md @@ -1,4 +1,4 @@ ---- +--- title: "12.2. Reactive Negotiation" rfc_number: 9110 rfc_section: "12.2" @@ -47,4 +47,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/58_12_3_request_content_negotiation.md b/notes/RFC/RFC9110/sections/58_12_3_request_content_negotiation.md index a831b7d0a..2b31878b8 100644 --- a/notes/RFC/RFC9110/sections/58_12_3_request_content_negotiation.md +++ b/notes/RFC/RFC9110/sections/58_12_3_request_content_negotiation.md @@ -1,4 +1,4 @@ ---- +--- title: "12.3. Request Content Negotiation" rfc_number: 9110 rfc_section: "12.3" @@ -25,4 +25,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/59_12_4_content_negotiation_field_features.md b/notes/RFC/RFC9110/sections/59_12_4_content_negotiation_field_features.md index 5b3f12229..b5d87e717 100644 --- a/notes/RFC/RFC9110/sections/59_12_4_content_negotiation_field_features.md +++ b/notes/RFC/RFC9110/sections/59_12_4_content_negotiation_field_features.md @@ -1,4 +1,4 @@ ---- +--- title: "12.4. Content Negotiation Field Features" rfc_number: 9110 rfc_section: "12.4" @@ -45,14 +45,12 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte value of 0 means "not acceptable". If no "q" parameter is present, the default weight is 1. - ```abnf weight = OWS ";" OWS "q=" qvalue qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) ``` - > **MUST NOT**: A sender of qvalue MUST NOT generate more than three digits after the decimal point. User configuration of these values ought to be limited in the same fashion. @@ -76,4 +74,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/60_12_5_content_negotiation_fields.md b/notes/RFC/RFC9110/sections/60_12_5_content_negotiation_fields.md index 7665a90f1..231fa8792 100644 --- a/notes/RFC/RFC9110/sections/60_12_5_content_negotiation_fields.md +++ b/notes/RFC/RFC9110/sections/60_12_5_content_negotiation_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "12.5. Content Negotiation Fields" rfc_number: 9110 rfc_section: "12.5" @@ -23,7 +23,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte about which content types are preferred in the content of a subsequent request to the same resource. - ```abnf Accept = #( media-range [ weight ] ) @@ -33,7 +32,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte ) parameters ``` - The asterisk "*" character is used to group media types into ranges, with "*/*" indicating all media types and "type/*" indicating all subtypes of that type. The media-range can include media type @@ -132,12 +130,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte capability to an origin server that is capable of representing information in those charsets. - ```abnf Accept-Charset = #( ( token / "*" ) [ weight ] ) ``` - > **MAY**: Charset names are defined in Section 8.3.2. A user agent MAY associate a quality value with each charset to indicate the user's relative preference for that charset, as defined in Section 12.4.2. @@ -170,13 +166,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte An "identity" token is used as a synonym for "no encoding" in order to communicate when no encoding is preferred. - ```abnf Accept-Encoding = #( codings [ weight ] ) codings = content-coding / "identity" / "*" ``` - > **MAY**: Each codings value MAY be given an associated quality value (weight) representing the preference for that encoding, as defined in Section 12.4.2. The asterisk "*" symbol in an Accept-Encoding field @@ -255,7 +249,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte indicate the set of natural languages that are preferred in the response. Language tags are defined in Section 8.5.1. - ```abnf Accept-Language = #( language-range [ weight ] ) ``` @@ -313,12 +306,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte influenced the origin server's process for selecting the content of this response. - ```abnf Vary = #( "*" / field-name ) ``` - A Vary field value is either the wildcard member "*" or a list of request field names, known as the selecting header fields, that might have had a role in selecting the representation for this response. @@ -379,4 +370,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/61_13_conditional_requests.md b/notes/RFC/RFC9110/sections/61_13_conditional_requests.md index 2805ac0fe..cbc868b79 100644 --- a/notes/RFC/RFC9110/sections/61_13_conditional_requests.md +++ b/notes/RFC/RFC9110/sections/61_13_conditional_requests.md @@ -1,4 +1,4 @@ ---- +--- title: "13. Conditional Requests" rfc_number: 9110 rfc_section: "13" @@ -73,12 +73,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte client intends this precondition to prevent the method from being applied if there have been any changes to the representation data. - ```abnf If-Match = "*" / #entity-tag ``` - Examples: If-Match: "xyzzy" @@ -109,4 +107,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/62_3_otherwise_the_condition_is_false.md b/notes/RFC/RFC9110/sections/62_3_otherwise_the_condition_is_false.md index 2eb1bacfd..9d002ca93 100644 --- a/notes/RFC/RFC9110/sections/62_3_otherwise_the_condition_is_false.md +++ b/notes/RFC/RFC9110/sections/62_3_otherwise_the_condition_is_false.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Otherwise, the condition is false." rfc_number: 9110 rfc_section: "3" @@ -65,12 +65,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte tags can be used for cache validation even if there have been changes to the representation data. - ```abnf If-None-Match = "*" / #entity-tag ``` - Examples: If-None-Match: "xyzzy" @@ -112,4 +110,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/63_3_otherwise_the_condition_is_true.md b/notes/RFC/RFC9110/sections/63_3_otherwise_the_condition_is_true.md index e61f2e194..9c5fb3995 100644 --- a/notes/RFC/RFC9110/sections/63_3_otherwise_the_condition_is_true.md +++ b/notes/RFC/RFC9110/sections/63_3_otherwise_the_condition_is_true.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Otherwise, the condition is true." rfc_number: 9110 rfc_section: "3" @@ -33,12 +33,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Transfer of the selected representation's data is avoided if that data has not changed. - ```abnf If-Modified-Since = HTTP-date ``` - An example of the field is: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT @@ -101,4 +99,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/64_2_otherwise_the_condition_is_true.md b/notes/RFC/RFC9110/sections/64_2_otherwise_the_condition_is_true.md index 72beaf6a2..7ff27cfab 100644 --- a/notes/RFC/RFC9110/sections/64_2_otherwise_the_condition_is_true.md +++ b/notes/RFC/RFC9110/sections/64_2_otherwise_the_condition_is_true.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Otherwise, the condition is true." rfc_number: 9110 rfc_section: "2" @@ -28,12 +28,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte This field accomplishes the same purpose as If-Match for cases where the user agent does not have an entity tag for the representation. - ```abnf If-Unmodified-Since = HTTP-date ``` - An example of the field is: If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT @@ -78,4 +76,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/65_2_otherwise_the_condition_is_false.md b/notes/RFC/RFC9110/sections/65_2_otherwise_the_condition_is_false.md index 01730f0c5..3cdaf3bad 100644 --- a/notes/RFC/RFC9110/sections/65_2_otherwise_the_condition_is_false.md +++ b/notes/RFC/RFC9110/sections/65_2_otherwise_the_condition_is_false.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Otherwise, the condition is false." rfc_number: 9110 rfc_section: "2" @@ -65,12 +65,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte representation is unchanged, send me the part(s) that I am requesting in Range; otherwise, send me the entire representation. - ```abnf If-Range = entity-tag / HTTP-date ``` - A valid entity-tag can be distinguished from a valid HTTP-date by examining the first three characters for a DQUOTE. @@ -102,4 +100,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/66_3_otherwise_the_condition_is_false.md b/notes/RFC/RFC9110/sections/66_3_otherwise_the_condition_is_false.md index e6a2fdf91..03ea1aa60 100644 --- a/notes/RFC/RFC9110/sections/66_3_otherwise_the_condition_is_false.md +++ b/notes/RFC/RFC9110/sections/66_3_otherwise_the_condition_is_false.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Otherwise, the condition is false." rfc_number: 9110 rfc_section: "3" @@ -20,4 +20,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/67_2_otherwise_the_condition_is_false.md b/notes/RFC/RFC9110/sections/67_2_otherwise_the_condition_is_false.md index 171583f60..fd9b86430 100644 --- a/notes/RFC/RFC9110/sections/67_2_otherwise_the_condition_is_false.md +++ b/notes/RFC/RFC9110/sections/67_2_otherwise_the_condition_is_false.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Otherwise, the condition is false." rfc_number: 9110 rfc_section: "2" @@ -120,4 +120,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/68_6_otherwise.md b/notes/RFC/RFC9110/sections/68_6_otherwise.md index 1c0ccfd33..a10384108 100644 --- a/notes/RFC/RFC9110/sections/68_6_otherwise.md +++ b/notes/RFC/RFC9110/sections/68_6_otherwise.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Otherwise," rfc_number: 9110 rfc_section: "6" @@ -21,4 +21,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/69_14_1_range_units.md b/notes/RFC/RFC9110/sections/69_14_1_range_units.md index 26670f5ac..aabeaae8a 100644 --- a/notes/RFC/RFC9110/sections/69_14_1_range_units.md +++ b/notes/RFC/RFC9110/sections/69_14_1_range_units.md @@ -1,4 +1,4 @@ ---- +--- title: "14.1. Range Units" rfc_number: 9110 rfc_section: "14.1" @@ -43,12 +43,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Content-Range (Section 14.4) header field to describe which part of a representation is being transferred. - ```abnf range-unit = token ``` - All range unit names are case-insensitive and ought to be registered within the "HTTP Range Unit Registry", as defined in Section 16.5.1. @@ -67,7 +65,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte A range request can specify a single range or a set of ranges within a single representation. - ```abnf ranges-specifier = range-unit "=" range-set range-set = 1#range-spec @@ -76,21 +73,18 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte / other-range ``` - An int-range is a range expressed as two non-negative integers or as one non-negative integer through to the end of the representation data. The range unit specifies what the integers mean (e.g., they might indicate unit offsets from the beginning, inclusive numbered parts, etc.). - ```abnf int-range = first-pos "-" [ last-pos ] first-pos = 1*DIGIT last-pos = 1*DIGIT ``` - An int-range is invalid if the last-pos value is present and less than the first-pos. @@ -98,24 +92,20 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte data with the provided non-negative integer maximum length (in range units). In other words, the last N units of the representation data. - ```abnf suffix-range = "-" suffix-length suffix-length = 1*DIGIT ``` - To provide for extensibility, the other-range rule is a mostly unconstrained grammar that allows application-specific or future range units to define additional range specifiers. - ```abnf other-range = 1*( %x21-2B / %x2D-7E ) ; 1*(VCHAR excluding comma) ``` - A ranges-specifier is invalid if it contains any range-spec that is invalid or undefined for the indicated range-unit. @@ -180,12 +170,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte * The first, middle, and last 1000 bytes: - ```abnf bytes= 0-999, 4500-5499, -1000 ``` - * Other valid (but not canonical) specifications of the second 500 bytes (byte offsets 500-999, inclusive): @@ -212,4 +200,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/70_14_2_range.md b/notes/RFC/RFC9110/sections/70_14_2_range.md index c1effabcc..3b14b6063 100644 --- a/notes/RFC/RFC9110/sections/70_14_2_range.md +++ b/notes/RFC/RFC9110/sections/70_14_2_range.md @@ -1,4 +1,4 @@ ---- +--- title: "14.2. Range" rfc_number: 9110 rfc_section: "14.2" @@ -16,12 +16,10 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte selected representation data (Section 8.1), rather than the entire selected representation. - ```abnf Range = ranges-specifier ``` - > **MAY**: A server MAY ignore the Range header field. However, origin servers and intermediate caches ought to support byte ranges when possible, since they support efficient recovery from partially failed transfers @@ -92,4 +90,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/71_14_3_accept-ranges.md b/notes/RFC/RFC9110/sections/71_14_3_accept-ranges.md index 65b0cc30c..c2b4966d8 100644 --- a/notes/RFC/RFC9110/sections/71_14_3_accept-ranges.md +++ b/notes/RFC/RFC9110/sections/71_14_3_accept-ranges.md @@ -1,4 +1,4 @@ ---- +--- title: "14.3. Accept-Ranges" rfc_number: 9110 rfc_section: "14.3" @@ -14,13 +14,11 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte The "Accept-Ranges" field in a response indicates whether an upstream server supports range requests for the target resource. - ```abnf Accept-Ranges = acceptable-ranges acceptable-ranges = 1#range-unit ``` - For example, a server that supports byte-range requests (Section 14.1.2) can send the field @@ -57,4 +55,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/72_14_4_content-range.md b/notes/RFC/RFC9110/sections/72_14_4_content-range.md index 35d027fb3..d78a654c1 100644 --- a/notes/RFC/RFC9110/sections/72_14_4_content-range.md +++ b/notes/RFC/RFC9110/sections/72_14_4_content-range.md @@ -1,4 +1,4 @@ ---- +--- title: "14.4. Content-Range" rfc_number: 9110 rfc_section: "14.4" @@ -19,7 +19,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte Satisfiable) responses to provide information about the selected representation. - ```abnf Content-Range = range-unit SP ( range-resp / unsatisfied-range ) @@ -31,7 +30,6 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte complete-length = 1*DIGIT ``` - If a 206 (Partial Content) response contains a Content-Range header field with a range unit (Section 14.1) that the recipient does not > **MUST NOT**: understand, the recipient MUST NOT attempt to recombine it with a @@ -102,4 +100,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/73_14_5_partial_put.md b/notes/RFC/RFC9110/sections/73_14_5_partial_put.md index 67adb6c3a..1a356e28b 100644 --- a/notes/RFC/RFC9110/sections/73_14_5_partial_put.md +++ b/notes/RFC/RFC9110/sections/73_14_5_partial_put.md @@ -1,4 +1,4 @@ ---- +--- title: "14.5. Partial PUT" rfc_number: 9110 rfc_section: "14.5" @@ -35,4 +35,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/74_14_6_media_type_multipartbyteranges.md b/notes/RFC/RFC9110/sections/74_14_6_media_type_multipartbyteranges.md index 190f1af83..7ecdd87a2 100644 --- a/notes/RFC/RFC9110/sections/74_14_6_media_type_multipartbyteranges.md +++ b/notes/RFC/RFC9110/sections/74_14_6_media_type_multipartbyteranges.md @@ -1,4 +1,4 @@ ---- +--- title: "14.6. Media Type multipart/byteranges" rfc_number: 9110 rfc_section: "14.6" @@ -103,4 +103,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/75_15_1_overview_of_status_codes.md b/notes/RFC/RFC9110/sections/75_15_1_overview_of_status_codes.md index 9eca892c2..b9392038d 100644 --- a/notes/RFC/RFC9110/sections/75_15_1_overview_of_status_codes.md +++ b/notes/RFC/RFC9110/sections/75_15_1_overview_of_status_codes.md @@ -1,4 +1,4 @@ ---- +--- title: "15.1. Overview of Status Codes" rfc_number: 9110 rfc_section: "15.1" @@ -77,24 +77,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte have been specified for use in HTTP. All such status codes ought to be registered within the "Hypertext Transfer Protocol (HTTP) Status Code Registry", as described in Section 16.2. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`HttpStatusCode.cs`** — Enum covering all standard status codes (100–599); unrecognized codes treated as x00 equivalent per §15.1 MUST requirement -- **`HttpResponseDecoder.cs`** — Parses three-digit status codes; rejects values outside 100–599 range -- **`StatusCodeClassification.cs`** — Classifies by first digit: informational, successful, redirection, client error, server error; handles interim (1xx) vs final responses - -### Test References -- `TurboHTTP.Tests/RFC9110/75_StatusCodeTests.cs` — Status code parsing, class-based fallback, invalid code handling - -### Known Gaps -- None - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/76_15_2_informational_1xx.md b/notes/RFC/RFC9110/sections/76_15_2_informational_1xx.md index 4e9f5907a..764ddcbf8 100644 --- a/notes/RFC/RFC9110/sections/76_15_2_informational_1xx.md +++ b/notes/RFC/RFC9110/sections/76_15_2_informational_1xx.md @@ -1,4 +1,4 @@ ---- +--- title: "15.2. Informational 1xx" rfc_number: 9110 rfc_section: "15.2" @@ -63,4 +63,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/77_15_3_successful_2xx.md b/notes/RFC/RFC9110/sections/77_15_3_successful_2xx.md index b510b7b67..602ace863 100644 --- a/notes/RFC/RFC9110/sections/77_15_3_successful_2xx.md +++ b/notes/RFC/RFC9110/sections/77_15_3_successful_2xx.md @@ -1,4 +1,4 @@ ---- +--- title: "15.3. Successful 2xx" rfc_number: 9110 rfc_section: "15.3" @@ -328,40 +328,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte response containing "multipart/byteranges" content, or multiple 206 (Partial Content) responses, each with one continuous range that is indicated by a Content-Range header field. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes -- **`Http11Decoder.cs`** / **`Http10Decoder.cs`** — Parse status-line and extract three-digit status code; 2xx codes flow through standard response path -- **`Http3ResponseDecoder.cs`** — Decodes `:status` pseudo-header for HTTP/3 2xx responses -- **`PartialContentValidator.cs`** — Validates 206 Partial Content responses: Content-Range parsing, single-part vs multipart detection per §15.3.7 -- **`ConnectionReuseEvaluator.cs`** — Treats 2xx as successful for connection reuse decisions -- **`CacheStore.cs`** — Stores heuristically cacheable 2xx responses (200, 203, 204, 206) per §15.3 cacheability rules - -### Compliance Details -| Sub-section | Status | Notes | -|-------------|--------|-------| -| §15.3.1 200 OK | ✅ Compliant | Fully parsed and handled across all protocol versions | -| §15.3.2 201 Created | ✅ Compliant | Location header extraction supported | -| §15.3.3 202 Accepted | ✅ Compliant | Passed through as standard response | -| §15.3.4 203 Non-Authoritative | ✅ Compliant | Heuristically cacheable per cache rules | -| §15.3.5 204 No Content | ✅ Compliant | Zero-length body enforced; cacheable | -| §15.3.6 205 Reset Content | ✅ Compliant | No content generated per MUST NOT | -| §15.3.7 206 Partial Content | ⚠️ Partial | Single-part Content-Range parsed; multipart/byteranges not fully supported | - -### Test References -- `TurboHTTP.Tests/RFC1945/12_RoundTripStatusCodeTests.cs` — HTTP/1.0 status code round-trips including 2xx -- `TurboHTTP.Tests/RFC9112/17_RoundTripStatusCodeTests.cs` — HTTP/1.1 status code round-trips including 2xx -- `TurboHTTP.StreamTests/RFC9112/09_Http11StatusCodeParsingTests.cs` — Status line parsing stage tests - -### Known Gaps -- 206 multipart/byteranges response assembly not implemented (§15.3.7.2) -- 206 range combining across multiple responses not implemented (§15.3.7.3) - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/78_15_4_redirection_3xx.md b/notes/RFC/RFC9110/sections/78_15_4_redirection_3xx.md index b85b566fd..2bb0c736b 100644 --- a/notes/RFC/RFC9110/sections/78_15_4_redirection_3xx.md +++ b/notes/RFC/RFC9110/sections/78_15_4_redirection_3xx.md @@ -1,4 +1,4 @@ ---- +--- title: "15.4. Redirection 3xx" rfc_number: 9110 rfc_section: "15.4" @@ -310,24 +310,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte | *Note:* This status code is much younger (June 2014) than its | sibling codes and thus might not be recognized everywhere. See | Section 4 of [RFC7538] for deployment considerations. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`RedirectStage.cs`** — Handles 301, 302, 303, 307, 308 redirects; resolves Location URI relative to original request; strips sensitive headers (Authorization, Cookie) on cross-origin redirects per §15.4 item 5 -- **`RedirectPolicy.cs`** — Configurable max redirect count (default 10) with cycle detection per §15.4 SHOULD requirement -- **`MethodTransformation.cs`** — POST→GET conversion for 301/302/303; method preservation for 307/308; strips content headers when method changes to GET/HEAD - -### Test References -- `TurboHTTP.Tests/RFC9110/78_RedirectTests.cs` — All redirect status codes, cross-origin header stripping, cycle detection, method transformation - -### Known Gaps -- ⚠️ 300 Multiple Choices — Not automatically handled; returned as-is to caller (no content parsing for alternatives) - ---- - -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/79_15_5_client_error_4xx.md b/notes/RFC/RFC9110/sections/79_15_5_client_error_4xx.md index 0ce689c86..c86c16d81 100644 --- a/notes/RFC/RFC9110/sections/79_15_5_client_error_4xx.md +++ b/notes/RFC/RFC9110/sections/79_15_5_client_error_4xx.md @@ -1,4 +1,4 @@ ---- +--- title: "15.5. Client Error 4xx" rfc_number: 9110 rfc_section: "15.5" @@ -331,4 +331,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/80_15_6_server_error_5xx.md b/notes/RFC/RFC9110/sections/80_15_6_server_error_5xx.md index 9a9f1d3db..cf832cec7 100644 --- a/notes/RFC/RFC9110/sections/80_15_6_server_error_5xx.md +++ b/notes/RFC/RFC9110/sections/80_15_6_server_error_5xx.md @@ -1,4 +1,4 @@ ---- +--- title: "15.6. Server Error 5xx" rfc_number: 9110 rfc_section: "15.6" @@ -76,4 +76,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/81_16_1_method_extensibility.md b/notes/RFC/RFC9110/sections/81_16_1_method_extensibility.md index cecf44a18..1c4aa5f7e 100644 --- a/notes/RFC/RFC9110/sections/81_16_1_method_extensibility.md +++ b/notes/RFC/RFC9110/sections/81_16_1_method_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.1. Method Extensibility" rfc_number: 9110 rfc_section: "16.1" @@ -104,4 +104,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/82_16_2_status_code_extensibility.md b/notes/RFC/RFC9110/sections/82_16_2_status_code_extensibility.md index a349e216f..3f9527b0f 100644 --- a/notes/RFC/RFC9110/sections/82_16_2_status_code_extensibility.md +++ b/notes/RFC/RFC9110/sections/82_16_2_status_code_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.2. Status Code Extensibility" rfc_number: 9110 rfc_section: "16.2" @@ -80,4 +80,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/83_16_3_field_extensibility.md b/notes/RFC/RFC9110/sections/83_16_3_field_extensibility.md index 16db3f832..682a7da82 100644 --- a/notes/RFC/RFC9110/sections/83_16_3_field_extensibility.md +++ b/notes/RFC/RFC9110/sections/83_16_3_field_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.3. Field Extensibility" rfc_number: 9110 rfc_section: "16.3" @@ -211,4 +211,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/84_16_4_authentication_scheme_extensibility.md b/notes/RFC/RFC9110/sections/84_16_4_authentication_scheme_extensibility.md index b58df81bc..7f651747a 100644 --- a/notes/RFC/RFC9110/sections/84_16_4_authentication_scheme_extensibility.md +++ b/notes/RFC/RFC9110/sections/84_16_4_authentication_scheme_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.4. Authentication Scheme Extensibility" rfc_number: 9110 rfc_section: "16.4" @@ -96,4 +96,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/85_16_5_range_unit_extensibility.md b/notes/RFC/RFC9110/sections/85_16_5_range_unit_extensibility.md index ca5177093..a57f8739f 100644 --- a/notes/RFC/RFC9110/sections/85_16_5_range_unit_extensibility.md +++ b/notes/RFC/RFC9110/sections/85_16_5_range_unit_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.5. Range Unit Extensibility" rfc_number: 9110 rfc_section: "16.5" @@ -38,4 +38,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/86_16_6_content_coding_extensibility.md b/notes/RFC/RFC9110/sections/86_16_6_content_coding_extensibility.md index 663966270..799f1307c 100644 --- a/notes/RFC/RFC9110/sections/86_16_6_content_coding_extensibility.md +++ b/notes/RFC/RFC9110/sections/86_16_6_content_coding_extensibility.md @@ -1,4 +1,4 @@ ---- +--- title: "16.6. Content Coding Extensibility" rfc_number: 9110 rfc_section: "16.6" @@ -44,4 +44,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/87_16_7_upgrade_token_registry.md b/notes/RFC/RFC9110/sections/87_16_7_upgrade_token_registry.md index d981f75f8..b7ae9944b 100644 --- a/notes/RFC/RFC9110/sections/87_16_7_upgrade_token_registry.md +++ b/notes/RFC/RFC9110/sections/87_16_7_upgrade_token_registry.md @@ -1,4 +1,4 @@ ---- +--- title: "16.7. Upgrade Token Registry" rfc_number: 9110 rfc_section: "16.7" @@ -25,4 +25,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist.md b/notes/RFC/RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist.md index 691003970..2788844fa 100644 --- a/notes/RFC/RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist.md +++ b/notes/RFC/RFC9110/sections/88_1_a_protocol-name_token_once_registered_stays_regist.md @@ -1,4 +1,4 @@ ---- +--- title: "1. A protocol-name token, once registered, stays registered forever." rfc_number: 9110 rfc_section: "1" @@ -19,4 +19,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/89_4_the_registration_must_name_a_point_of_contact.md b/notes/RFC/RFC9110/sections/89_4_the_registration_must_name_a_point_of_contact.md index 257f88bf1..2c73aa4d5 100644 --- a/notes/RFC/RFC9110/sections/89_4_the_registration_must_name_a_point_of_contact.md +++ b/notes/RFC/RFC9110/sections/89_4_the_registration_must_name_a_point_of_contact.md @@ -1,4 +1,4 @@ ---- +--- title: "4. The registration MUST name a point of contact." rfc_number: 9110 rfc_section: "4" @@ -27,4 +27,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/90_17_1_establishing_authority.md b/notes/RFC/RFC9110/sections/90_17_1_establishing_authority.md index 8c9870b17..4ef1e46c5 100644 --- a/notes/RFC/RFC9110/sections/90_17_1_establishing_authority.md +++ b/notes/RFC/RFC9110/sections/90_17_1_establishing_authority.md @@ -1,4 +1,4 @@ ---- +--- title: "17.1. Establishing Authority" rfc_number: 9110 rfc_section: "17.1" @@ -84,4 +84,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/91_17_2_risks_of_intermediaries.md b/notes/RFC/RFC9110/sections/91_17_2_risks_of_intermediaries.md index 842f75b32..f2717e689 100644 --- a/notes/RFC/RFC9110/sections/91_17_2_risks_of_intermediaries.md +++ b/notes/RFC/RFC9110/sections/91_17_2_risks_of_intermediaries.md @@ -1,4 +1,4 @@ ---- +--- title: "17.2. Risks of Intermediaries" rfc_number: 9110 rfc_section: "17.2" @@ -34,4 +34,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/92_17_3_attacks_based_on_file_and_path_names.md b/notes/RFC/RFC9110/sections/92_17_3_attacks_based_on_file_and_path_names.md index 0298bca05..99874c4c2 100644 --- a/notes/RFC/RFC9110/sections/92_17_3_attacks_based_on_file_and_path_names.md +++ b/notes/RFC/RFC9110/sections/92_17_3_attacks_based_on_file_and_path_names.md @@ -1,4 +1,4 @@ ---- +--- title: "17.3. Attacks Based on File and Path Names" rfc_number: 9110 rfc_section: "17.3" @@ -35,4 +35,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/93_17_4_attacks_based_on_command_code_or_query_injection.md b/notes/RFC/RFC9110/sections/93_17_4_attacks_based_on_command_code_or_query_injection.md index 9f846ec2b..d7c9cb985 100644 --- a/notes/RFC/RFC9110/sections/93_17_4_attacks_based_on_command_code_or_query_injection.md +++ b/notes/RFC/RFC9110/sections/93_17_4_attacks_based_on_command_code_or_query_injection.md @@ -1,4 +1,4 @@ ---- +--- title: "17.4. Attacks Based on Command, Code, or Query Injection" rfc_number: 9110 rfc_section: "17.4" @@ -42,4 +42,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/94_17_5_attacks_via_protocol_element_length.md b/notes/RFC/RFC9110/sections/94_17_5_attacks_via_protocol_element_length.md index eb1ee934b..c051fe6d7 100644 --- a/notes/RFC/RFC9110/sections/94_17_5_attacks_via_protocol_element_length.md +++ b/notes/RFC/RFC9110/sections/94_17_5_attacks_via_protocol_element_length.md @@ -1,4 +1,4 @@ ---- +--- title: "17.5. Attacks via Protocol Element Length" rfc_number: 9110 rfc_section: "17.5" @@ -36,4 +36,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/95_17_6_attacks_using_shared-dictionary_compression.md b/notes/RFC/RFC9110/sections/95_17_6_attacks_using_shared-dictionary_compression.md index e67dc3f62..07c3bf691 100644 --- a/notes/RFC/RFC9110/sections/95_17_6_attacks_using_shared-dictionary_compression.md +++ b/notes/RFC/RFC9110/sections/95_17_6_attacks_using_shared-dictionary_compression.md @@ -1,4 +1,4 @@ ---- +--- title: "17.6. Attacks Using Shared-Dictionary Compression" rfc_number: 9110 rfc_section: "17.6" @@ -32,4 +32,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/96_17_7_disclosure_of_personal_information.md b/notes/RFC/RFC9110/sections/96_17_7_disclosure_of_personal_information.md index 33e71db35..573b4aae8 100644 --- a/notes/RFC/RFC9110/sections/96_17_7_disclosure_of_personal_information.md +++ b/notes/RFC/RFC9110/sections/96_17_7_disclosure_of_personal_information.md @@ -1,4 +1,4 @@ ---- +--- title: "17.7. Disclosure of Personal Information" rfc_number: 9110 rfc_section: "17.7" @@ -20,4 +20,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/97_17_8_privacy_of_server_log_information.md b/notes/RFC/RFC9110/sections/97_17_8_privacy_of_server_log_information.md index d005457ce..ffc31fbcf 100644 --- a/notes/RFC/RFC9110/sections/97_17_8_privacy_of_server_log_information.md +++ b/notes/RFC/RFC9110/sections/97_17_8_privacy_of_server_log_information.md @@ -1,4 +1,4 @@ ---- +--- title: "17.8. Privacy of Server Log Information" rfc_number: 9110 rfc_section: "17.8" @@ -35,4 +35,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/98_17_9_disclosure_of_sensitive_information_in_uris.md b/notes/RFC/RFC9110/sections/98_17_9_disclosure_of_sensitive_information_in_uris.md index eedc4450d..3cd33ac64 100644 --- a/notes/RFC/RFC9110/sections/98_17_9_disclosure_of_sensitive_information_in_uris.md +++ b/notes/RFC/RFC9110/sections/98_17_9_disclosure_of_sensitive_information_in_uris.md @@ -1,4 +1,4 @@ ---- +--- title: "17.9. Disclosure of Sensitive Information in URIs" rfc_number: 9110 rfc_section: "17.9" @@ -44,4 +44,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/99_17_10_application_handling_of_field_names.md b/notes/RFC/RFC9110/sections/99_17_10_application_handling_of_field_names.md index 883ff59ac..36c697e9a 100644 --- a/notes/RFC/RFC9110/sections/99_17_10_application_handling_of_field_names.md +++ b/notes/RFC/RFC9110/sections/99_17_10_application_handling_of_field_names.md @@ -1,4 +1,4 @@ ---- +--- title: "17.10. Application Handling of Field Names" rfc_number: 9110 rfc_section: "17.10" @@ -55,4 +55,3 @@ tags: [RFC9110, HTTP-semantics, methods, status-codes, redirects, retries, conte --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9110/sections/99_acknowledgements.md b/notes/RFC/RFC9110/sections/99_acknowledgements.md index 6203a027c..61bfb5a85 100644 --- a/notes/RFC/RFC9110/sections/99_acknowledgements.md +++ b/notes/RFC/RFC9110/sections/99_acknowledgements.md @@ -1,4 +1,4 @@ ---- +--- title: "Acknowledgements" rfc_number: 9110 rfc_section: "-" @@ -57,4 +57,3 @@ Acknowledgements --- -**Navigation:** [[../RFC9110|RFC9110 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/RFC9111.md b/notes/RFC/RFC9111/RFC9111.md index 8a4759e37..a668f3a95 100644 --- a/notes/RFC/RFC9111/RFC9111.md +++ b/notes/RFC/RFC9111/RFC9111.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9111 — HTTP Caching" rfc_number: 9111 description: "HTTP caching model for shared and private caches. Defines freshness lifetime, validation via conditional requests, Cache-Control directives, and Vary-based secondary keys." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" **Official RFC**: [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 78/100 | -| **Implementation Status** | ✅ Complete | -| **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 - [[RFC9111/sections/03_2_overview_of_cache_operation|§2 Cache Operation Overview]] — how caches store and retrieve responses @@ -30,32 +19,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" - [[RFC9111/sections/10_5_2_cache-control|§5.2 Cache-Control]] — directive parsing and semantics - [[RFC9111/sections/05_4_1_calculating_cache_keys_with_the_vary_header_field|§4.1 Vary]] — secondary cache keys -## Implementation Notes - -### Protocol Components - -| Component | File | Purpose | -|-----------|------|---------| -| `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 - -| Stage | File | Purpose | -|-------|------|---------| -| `CacheBidiStage` | `Streams/Stages/Features/CacheBidiStage.cs` | Cache lookup and storage in stream pipeline | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/Caching/` | Unit tests — freshness, validation, storage, directives, qualified directives | -| `TurboHTTP.StreamTests/Caching/` | Stage behaviour tests — cache lookup, storage, and shared response | - ## Sections | # | Section | File | Status | @@ -92,7 +55,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9111" - [[RFC9110/RFC9110|RFC 9110 — HTTP Semantics]] — core HTTP semantics - [[RFC9112/RFC9112|RFC 9112 — HTTP/1.1]] — message framing -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking --- diff --git a/notes/RFC/RFC9111/sections/00_preamble.md b/notes/RFC/RFC9111/sections/00_preamble.md index 7cd5b7a7a..66d3893cd 100644 --- a/notes/RFC/RFC9111/sections/00_preamble.md +++ b/notes/RFC/RFC9111/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9111 rfc_section: "preamble" @@ -9,10 +9,6 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp ## Preamble - - - - Internet Engineering Task Force (IETF) R. Fielding, Ed. Request for Comments: 9111 Adobe STD: 98 M. Nottingham, Ed. @@ -21,7 +17,6 @@ Category: Standards Track J. Reschke, Ed. ISSN: 2070-1721 greenbytes June 2022 - HTTP Caching Abstract @@ -150,4 +145,3 @@ Table of Contents --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/02_1_introduction.md b/notes/RFC/RFC9111/sections/02_1_introduction.md index 138bcbc12..e95375d60 100644 --- a/notes/RFC/RFC9111/sections/02_1_introduction.md +++ b/notes/RFC/RFC9111/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9111 rfc_section: "1" @@ -75,7 +75,6 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp [HTTP] defines the following rules: - ```abnf HTTP-date = OWS = @@ -84,18 +83,15 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp token = ``` - ### 1.2.2 Delta Seconds The delta-seconds rule specifies a non-negative integer, representing time in seconds. - ```abnf delta-seconds = 1*DIGIT ``` - A recipient parsing a delta-seconds value and converting it to binary form ought to use an arithmetic type of at least 31 bits of non- negative integer range. If a cache receives a delta-seconds value @@ -115,4 +111,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/03_2_overview_of_cache_operation.md b/notes/RFC/RFC9111/sections/03_2_overview_of_cache_operation.md index f2e558ba6..941756697 100644 --- a/notes/RFC/RFC9111/sections/03_2_overview_of_cache_operation.md +++ b/notes/RFC/RFC9111/sections/03_2_overview_of_cache_operation.md @@ -1,4 +1,4 @@ ---- +--- title: 2. Overview of Cache Operation rfc_number: 9111 rfc_section: '2' @@ -64,28 +64,3 @@ tags: A cache is "disconnected" when it cannot contact the origin server or otherwise find a forward path for a request. A disconnected cache can serve stale responses in some circumstances (Section 4.2.4). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not implement an HTTP cache. The client library forwards all requests directly to the origin server without any cache lookup, storage, or revalidation logic. CacheLookupStage is planned as a future pipeline stage but not yet implemented. - -**Key Gaps:** -- No cache storage or retrieval mechanism -- No freshness evaluation or expiration logic -- No private vs shared cache distinction -- No understanding of cacheable methods or status codes -- No response reuse logic - -**Affected Components:** None (no caching components exist) - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/04_3_storing_responses_in_caches.md b/notes/RFC/RFC9111/sections/04_3_storing_responses_in_caches.md index db6c094fb..7a4d79fa8 100644 --- a/notes/RFC/RFC9111/sections/04_3_storing_responses_in_caches.md +++ b/notes/RFC/RFC9111/sections/04_3_storing_responses_in_caches.md @@ -1,4 +1,4 @@ ---- +--- title: 3. Storing Responses in Caches rfc_number: 9111 rfc_section: '3' @@ -202,27 +202,3 @@ tags: In this specification, the following response directives have such an effect: must-revalidate (Section 5.2.2.2), public (Section 5.2.2.9), and s-maxage (Section 5.2.2.10). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not store responses in any cache. No logic exists to evaluate whether a response is cacheable based on request method, status code, or Cache-Control directives. All responses are passed directly to the caller without storage consideration. - -**Key Gaps:** -- No response storage mechanism -- No evaluation of `no-store`, `private`, or `Authorization` constraints -- No incomplete response handling for caching purposes -- No `s-maxage` or shared cache directive processing - -**Affected Components:** None - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/05_4_1_calculating_cache_keys_with_the_vary_header_field.md b/notes/RFC/RFC9111/sections/05_4_1_calculating_cache_keys_with_the_vary_header_field.md index f2a55a413..9a579b347 100644 --- a/notes/RFC/RFC9111/sections/05_4_1_calculating_cache_keys_with_the_vary_header_field.md +++ b/notes/RFC/RFC9111/sections/05_4_1_calculating_cache_keys_with_the_vary_header_field.md @@ -1,4 +1,4 @@ ---- +--- title: 4.1. Calculating Cache Keys with the Vary Header Field rfc_number: 9111 rfc_section: '4.1' @@ -135,27 +135,3 @@ tags: request. Typically, the request is forwarded to the origin server, potentially with preconditions added to describe what responses the cache has already stored (Section 4.3). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not compute cache keys or process the Vary header field for cache selection purposes. The Vary header is passed through in responses but not used for any storage or retrieval logic. - -**Key Gaps:** -- No cache key computation from effective request URI -- No Vary-based secondary key selection -- No `Vary: *` handling -- No stored response matching against request header fields - -**Affected Components:** None - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/06_4_2_freshness.md b/notes/RFC/RFC9111/sections/06_4_2_freshness.md index b7f75b3c3..afe9bfe07 100644 --- a/notes/RFC/RFC9111/sections/06_4_2_freshness.md +++ b/notes/RFC/RFC9111/sections/06_4_2_freshness.md @@ -1,4 +1,4 @@ ---- +--- title: 4.2. Freshness rfc_number: 9111 rfc_section: '4.2' @@ -237,29 +237,3 @@ tags: (e.g., by the max-stale request directive in Section 5.2.1, extension directives such as those defined in [RFC5861], or configuration in accordance with an out-of-band contract). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not perform freshness calculations. No age computation, freshness lifetime evaluation, or stale response serving logic exists. The client does not interpret `max-age`, `s-maxage`, `Expires`, or heuristic freshness rules. - -**Key Gaps:** -- No age calculation algorithm (§4.2.3) -- No freshness lifetime computation from `max-age` or `Expires` -- No heuristic freshness estimation -- No stale response serving with `stale-while-revalidate` or `stale-if-error` -- No `min-fresh` or `max-stale` request directive handling -- No `Age` header generation - -**Affected Components:** None - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/07_4_3_validation.md b/notes/RFC/RFC9111/sections/07_4_3_validation.md index 1b8ef9032..6de9a8cc2 100644 --- a/notes/RFC/RFC9111/sections/07_4_3_validation.md +++ b/notes/RFC/RFC9111/sections/07_4_3_validation.md @@ -1,4 +1,4 @@ ---- +--- title: 4.3. Validation rfc_number: 9111 rfc_section: '4.3' @@ -233,28 +233,3 @@ tags: If a cache updates a stored response with the metadata provided in a > **MUST**: HEAD response, the cache MUST use the header fields provided in the HEAD response to update the stored response (see Section 3.2). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not perform cache validation. No conditional request generation (If-None-Match, If-Modified-Since) for cache revalidation exists. The client does not send conditional requests automatically to revalidate stale cached responses, nor does it process 304 (Not Modified) responses for cache update purposes. - -**Key Gaps:** -- No conditional request generation for revalidation -- No 304 response handling for cache freshening -- No ETag/Last-Modified based validator comparison -- No HEAD request cache update logic -- No handling of partial 200 responses invalidating partial cache entries - -**Affected Components:** None - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/08_4_4_invalidating_stored_responses.md b/notes/RFC/RFC9111/sections/08_4_4_invalidating_stored_responses.md index c2672a123..311e63704 100644 --- a/notes/RFC/RFC9111/sections/08_4_4_invalidating_stored_responses.md +++ b/notes/RFC/RFC9111/sections/08_4_4_invalidating_stored_responses.md @@ -1,4 +1,4 @@ ---- +--- title: 4.4. Invalidating Stored Responses rfc_number: 9111 rfc_section: '4.4' @@ -52,26 +52,3 @@ tags: Note that this does not guarantee that all appropriate responses are invalidated globally; a state-changing request would only invalidate responses in the caches it travels through. - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not implement cache invalidation. No logic exists to invalidate stored responses after successful unsafe method requests (POST, PUT, DELETE). Since no cache storage exists, there is nothing to invalidate. - -**Key Gaps:** -- No invalidation triggered by unsafe methods -- No invalidation based on Location/Content-Location headers -- No protection against invalidation from non-trustworthy sources - -**Affected Components:** None - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/09_5_1_age.md b/notes/RFC/RFC9111/sections/09_5_1_age.md index d5fe249fe..9c98b4ba8 100644 --- a/notes/RFC/RFC9111/sections/09_5_1_age.md +++ b/notes/RFC/RFC9111/sections/09_5_1_age.md @@ -1,4 +1,4 @@ ---- +--- title: 5.1. Age rfc_number: 9111 rfc_section: '5.1' @@ -51,26 +51,3 @@ tags: generated or validated by the origin server for this request. However, lack of an Age header field does not imply the origin was contacted. - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not generate or consume the Age header field for caching purposes. The Age header is passed through in responses as a standard header but is not interpreted or used for freshness calculations. - -**Key Gaps:** -- No Age header generation -- No Age value interpretation for cache freshness -- No delta-seconds parsing specific to Age - -**Affected Components:** None (Age header passed through as generic header) - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/10_5_2_cache-control.md b/notes/RFC/RFC9111/sections/10_5_2_cache-control.md index b123933c7..2e2dc882f 100644 --- a/notes/RFC/RFC9111/sections/10_5_2_cache-control.md +++ b/notes/RFC/RFC9111/sections/10_5_2_cache-control.md @@ -1,4 +1,4 @@ ---- +--- title: 5.2. Cache-Control rfc_number: 9111 rfc_section: '5.2' @@ -410,31 +410,3 @@ tags: Values to be added to this namespace require IETF Review (see [RFC8126], Section 4.8). - - ---- - -## TurboHTTP Compliance - -**Status:** ❌ Missing - -**Implementation Notes:** -TurboHTTP does not parse or act on Cache-Control directives. The Cache-Control header is passed through in requests and responses as a standard header but no directive-specific logic exists. The client does not honor `no-cache`, `no-store`, `max-age`, `must-revalidate`, or any other cache directives. - -**Key Gaps:** -- No Cache-Control directive parsing -- No `no-cache` / `no-store` enforcement -- No `max-age` / `s-maxage` freshness calculation -- No `must-revalidate` / `proxy-revalidate` logic -- No `public` / `private` response classification -- No `no-transform` enforcement -- No `only-if-cached` request directive handling -- No extension directive support - -**Affected Components:** None (Cache-Control header passed through as generic header) - -**Test References:** None - ---- - -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/11_5_3_expires.md b/notes/RFC/RFC9111/sections/11_5_3_expires.md index 7de9a0451..8b8bfb42d 100644 --- a/notes/RFC/RFC9111/sections/11_5_3_expires.md +++ b/notes/RFC/RFC9111/sections/11_5_3_expires.md @@ -1,4 +1,4 @@ ---- +--- title: "5.3. Expires" rfc_number: 9111 rfc_section: "5.3" @@ -23,12 +23,10 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp Section 5.6.7 of [HTTP]. See also Section 4.2 for parsing requirements specific to caches. - ```abnf Expires = HTTP-date ``` - For example Expires: Thu, 01 Dec 1994 16:00:00 GMT @@ -59,4 +57,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/12_5_4_pragma.md b/notes/RFC/RFC9111/sections/12_5_4_pragma.md index faad14bb8..3428a8e30 100644 --- a/notes/RFC/RFC9111/sections/12_5_4_pragma.md +++ b/notes/RFC/RFC9111/sections/12_5_4_pragma.md @@ -1,4 +1,4 @@ ---- +--- title: "5.4. Pragma" rfc_number: 9111 rfc_section: "5.4" @@ -24,4 +24,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/13_5_5_warning.md b/notes/RFC/RFC9111/sections/13_5_5_warning.md index 2210cbb98..403099d8e 100644 --- a/notes/RFC/RFC9111/sections/13_5_5_warning.md +++ b/notes/RFC/RFC9111/sections/13_5_5_warning.md @@ -1,4 +1,4 @@ ---- +--- title: "5.5. Warning" rfc_number: 9111 rfc_section: "5.5" @@ -20,4 +20,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/14_6_relationship_to_applications_and_other_caches.md b/notes/RFC/RFC9111/sections/14_6_relationship_to_applications_and_other_caches.md index 883bf3211..d53e4286c 100644 --- a/notes/RFC/RFC9111/sections/14_6_relationship_to_applications_and_other_caches.md +++ b/notes/RFC/RFC9111/sections/14_6_relationship_to_applications_and_other_caches.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Relationship to Applications and Other Caches" rfc_number: 9111 rfc_section: "6" @@ -45,4 +45,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/15_7_security_considerations.md b/notes/RFC/RFC9111/sections/15_7_security_considerations.md index dd51ec311..10d128452 100644 --- a/notes/RFC/RFC9111/sections/15_7_security_considerations.md +++ b/notes/RFC/RFC9111/sections/15_7_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Security Considerations" rfc_number: 9111 rfc_section: "7" @@ -76,4 +76,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/16_8_iana_considerations.md b/notes/RFC/RFC9111/sections/16_8_iana_considerations.md index e0b075a13..b0ac09e5b 100644 --- a/notes/RFC/RFC9111/sections/16_8_iana_considerations.md +++ b/notes/RFC/RFC9111/sections/16_8_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "8. IANA Considerations" rfc_number: 9111 rfc_section: "8" @@ -87,4 +87,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/86_9_references.md b/notes/RFC/RFC9111/sections/86_9_references.md index c74923539..8721270e6 100644 --- a/notes/RFC/RFC9111/sections/86_9_references.md +++ b/notes/RFC/RFC9111/sections/86_9_references.md @@ -1,4 +1,4 @@ ---- +--- title: "9. References" rfc_number: 9111 rfc_section: "9" @@ -68,4 +68,3 @@ tags: [RFC9111, HTTP-caching, freshness, validation, Cache-Control, max-age, Exp --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/91_appendix_a_collected_abnf.md b/notes/RFC/RFC9111/sections/91_appendix_a_collected_abnf.md index 1dd3d8517..49bb9f1f1 100644 --- a/notes/RFC/RFC9111/sections/91_appendix_a_collected_abnf.md +++ b/notes/RFC/RFC9111/sections/91_appendix_a_collected_abnf.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Collected ABNF" rfc_number: 9111 rfc_section: "Appendix A" @@ -14,7 +14,6 @@ Appendix A. Collected ABNF In the collected ABNF below, list rules are expanded per Section 5.6.1 of [HTTP]. - ```abnf Age = delta-seconds @@ -39,4 +38,3 @@ Appendix A. Collected ABNF --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/92_appendix_b_changes_from_rfc_7234.md b/notes/RFC/RFC9111/sections/92_appendix_b_changes_from_rfc_7234.md index 1efefc469..21d05a7d1 100644 --- a/notes/RFC/RFC9111/sections/92_appendix_b_changes_from_rfc_7234.md +++ b/notes/RFC/RFC9111/sections/92_appendix_b_changes_from_rfc_7234.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Changes from RFC 7234" rfc_number: 9111 rfc_section: "Appendix B" @@ -47,4 +47,3 @@ Appendix B. Changes from RFC 7234 --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9111/sections/99_acknowledgements.md b/notes/RFC/RFC9111/sections/99_acknowledgements.md index 66955913f..94173d9f1 100644 --- a/notes/RFC/RFC9111/sections/99_acknowledgements.md +++ b/notes/RFC/RFC9111/sections/99_acknowledgements.md @@ -1,4 +1,4 @@ ---- +--- title: "Acknowledgements" rfc_number: 9111 rfc_section: "-" @@ -16,4 +16,3 @@ Acknowledgements --- -**Navigation:** [[../RFC9111|RFC9111 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/RFC9112.md b/notes/RFC/RFC9112/RFC9112.md index f823e9209..49bf38eda 100644 --- a/notes/RFC/RFC9112/RFC9112.md +++ b/notes/RFC/RFC9112/RFC9112.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9112 — HTTP/1.1" rfc_number: 9112 description: "HTTP/1.1 message syntax and connection management. Defines request-line, Host header, chunked transfer coding, persistent connections, and pipelining." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9112" **Official RFC**: [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 92/100 | -| **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9112/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9112/` — 26 files, 374 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9112/` | -| **Key Gaps** | Chunk extensions, trailer headers, obsolete-text header strictness | - ## Core Concepts - [[RFC9112/sections/04_3_request_line|§3 Request Line]] — request-line format (method SP request-target SP HTTP-version) @@ -30,39 +19,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9112" - [[RFC9112/sections/06_5_field_syntax|§5 Field Syntax]] — header field format (name ":" OWS value) - [[RFC9112/sections/09_8_handling_incomplete_messages|§8 Incomplete Messages]] — error handling for truncated responses -## Implementation Notes - -### Encoder - -| Component | File | Purpose | -|-----------|------|---------| -| `Http11Encoder` | `Protocol/RFC9112/Http11Encoder.cs` | Request serialization with Host header, Content-Length, chunked | - -### Decoder - -| Component | File | Purpose | -|-----------|------|---------| -| `Http11DecoderPipeline` | `Protocol/RFC9112/Http11DecoderPipeline.cs` | Stateful response parsing with remainder handling | -| `Http11EventAggregator` | `Protocol/RFC9112/Http11EventAggregator.cs` | Event stream → HttpResponseMessage assembly | -| `Http11CompletionDecoder` | `Protocol/RFC9112/Http11CompletionDecoder.cs` | Convenience wrapper for complete response decoding | -| `ConnectionReuseEvaluator` | `Protocol/RFC9112/ConnectionReuseEvaluator.cs` | §9.3 keep-alive/close decision | - -### Stages - -| Stage | File | Purpose | -|-------|------|---------| -| `Http11EncoderStage` | `Streams/Stages/Encoding/Http11EncoderStage.cs` | Request encoding in stream pipeline | -| `Http11DecoderStage` | `Streams/Stages/Decoding/Http11DecoderStage.cs` | Response decoding in stream pipeline | -| `Http1XCorrelationStage` | `Streams/Stages/Routing/Http1XCorrelationStage.cs` | FIFO request-response correlation | -| `ConnectionReuseStage` | `Streams/Stages/Features/ConnectionReuseStage.cs` | Keep-alive/close decisions in pipeline | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/RFC9112/` | 374 unit tests — encoder, decoder, chunked, connection reuse | -| `TurboHTTP.StreamTests/RFC9112/` | Stage behaviour tests — encoder, decoder, correlation, connection stages | - ## Sections | # | Section | File | Status | @@ -106,7 +62,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9112" - [[RFC1945/RFC1945|RFC 1945 — HTTP/1.0]] — predecessor protocol - [[RFC9110/RFC9110|RFC 9110 — HTTP Semantics]] — shared semantics - [[RFC9113/RFC9113|RFC 9113 — HTTP/2]] — binary successor protocol -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking --- diff --git a/notes/RFC/RFC9112/sections/00_preamble.md b/notes/RFC/RFC9112/sections/00_preamble.md index b73b1aec3..fca4b795d 100644 --- a/notes/RFC/RFC9112/sections/00_preamble.md +++ b/notes/RFC/RFC9112/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9112 rfc_section: "preamble" @@ -9,10 +9,6 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme ## Preamble - - - - Internet Engineering Task Force (IETF) R. Fielding, Ed. Request for Comments: 9112 Adobe STD: 99 M. Nottingham, Ed. @@ -21,7 +17,6 @@ Category: Standards Track J. Reschke, Ed. ISSN: 2070-1721 greenbytes June 2022 - HTTP/1.1 Abstract @@ -157,4 +152,3 @@ Table of Contents --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/02_1_introduction.md b/notes/RFC/RFC9112/sections/02_1_introduction.md index bbd563563..d255e246a 100644 --- a/notes/RFC/RFC9112/sections/02_1_introduction.md +++ b/notes/RFC/RFC9112/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9112 rfc_section: "1" @@ -67,7 +67,6 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme The rules below are defined in [HTTP]: - ```abnf BWS = OWS = @@ -85,7 +84,6 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme The rules below are defined in [URI]: - ```abnf absolute-URI = authority = @@ -96,4 +94,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/03_2_message.md b/notes/RFC/RFC9112/sections/03_2_message.md index 1d5c78f90..a7cde5dc0 100644 --- a/notes/RFC/RFC9112/sections/03_2_message.md +++ b/notes/RFC/RFC9112/sections/03_2_message.md @@ -1,4 +1,4 @@ ---- +--- title: 2. Message rfc_number: 9112 rfc_section: '2' @@ -174,34 +174,3 @@ tags: unless triggered by specific client attributes, such as when one or more of the request header fields (e.g., User-Agent) uniquely match the values sent by a client known to be in error. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP's `Http11ResponseDecoder` and `Http11RequestEncoder` implement HTTP/1.1 message framing per §2. Messages are parsed as octet sequences (not Unicode strings). The decoder handles start-line parsing, header field extraction, and body length determination. CRLF line terminators are required; bare LF tolerance is implemented for robustness. Bare CR characters within protocol elements are rejected. - -**Key Components:** -- `Http11ResponseDecoder` — parses status-line, headers, and body from byte stream -- `Http11RequestEncoder` — generates request-line, headers, and body framing -- `Http11MessageParser` — low-level ABNF-compliant parsing utilities - -**Compliance Details:** -- ✅ Parses as octet sequence (US-ASCII superset), not Unicode -- ✅ CRLF line termination enforced -- ✅ Bare CR handling (reject/replace) -- ✅ No extra CRLF before/after requests -- ✅ HTTP-version parsing and generation -- ✅ Whitespace between start-line and headers rejected - -**Gaps:** None identified - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/04_3_request_line.md b/notes/RFC/RFC9112/sections/04_3_request_line.md index e3f197b93..38901266b 100644 --- a/notes/RFC/RFC9112/sections/04_3_request_line.md +++ b/notes/RFC/RFC9112/sections/04_3_request_line.md @@ -1,4 +1,4 @@ ---- +--- title: 3. Request Line rfc_number: 9112 rfc_section: '3' @@ -319,37 +319,3 @@ tags: determining whether that target URI identifies a resource for which the server is willing and able to send a response, as defined in Section 7.4 of [HTTP]. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP's `Http11RequestEncoder` generates compliant request-lines with method, request-target (origin-form), and HTTP-version. The Host header is always included in HTTP/1.1 requests. Request-target is derived from the target URI using origin-form (absolute-path + query). - -**Key Components:** -- `Http11RequestEncoder` — generates `method SP request-target SP HTTP-version CRLF` -- `HttpRequestEncoder` — prepares request metadata including Host header - -**Compliance Details:** -- ✅ Request-line format: `method SP request-target SP HTTP-version` -- ✅ Host header always sent in HTTP/1.1 requests -- ✅ Origin-form used for direct requests (absolute-path + query) -- ✅ Empty path normalized to "/" -- ⚠️ Absolute-form (proxy requests) not currently used (TurboHTTP is not a proxy client) -- ⚠️ Authority-form (CONNECT) not supported -- ⚠️ Asterisk-form (OPTIONS *) not supported - -**Gaps:** -- No proxy-style absolute-form request-target -- No CONNECT method support -- No OPTIONS * (server-wide) support - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/05_4_status_line.md b/notes/RFC/RFC9112/sections/05_4_status_line.md index b8842b02b..08a081235 100644 --- a/notes/RFC/RFC9112/sections/05_4_status_line.md +++ b/notes/RFC/RFC9112/sections/05_4_status_line.md @@ -1,4 +1,4 @@ ---- +--- title: 4. Status Line rfc_number: 9112 rfc_section: '4' @@ -79,31 +79,3 @@ tags: space that separates the status-code from the reason-phrase even when the reason-phrase is absent (i.e., the status-line would end with the space). - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP's `Http11ResponseDecoder` parses status-lines per §4. The decoder extracts HTTP-version, 3-digit status code, and optional reason-phrase. The reason-phrase is parsed but not used for application logic (as recommended by the RFC). Status codes are mapped to `HttpStatusCode` enum values. - -**Key Components:** -- `Http11ResponseDecoder` — parses `HTTP-version SP status-code SP [reason-phrase]` -- `HttpStatusCode` — enum covering all standard status codes - -**Compliance Details:** -- ✅ Status-line parsing: HTTP-version, status-code, reason-phrase -- ✅ 3-digit status-code extraction -- ✅ Reason-phrase ignored for logic (used for display only) -- ✅ Whitespace-delimited parsing with robustness - -**Gaps:** None identified - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/06_5_field_syntax.md b/notes/RFC/RFC9112/sections/06_5_field_syntax.md index e78c58429..916c95b70 100644 --- a/notes/RFC/RFC9112/sections/06_5_field_syntax.md +++ b/notes/RFC/RFC9112/sections/06_5_field_syntax.md @@ -1,4 +1,4 @@ ---- +--- title: 5. Field Syntax rfc_number: 9112 rfc_section: '5' @@ -96,32 +96,3 @@ tags: > **MUST**: not within a "message/http" container MUST replace each received obs-fold with one or more SP octets prior to interpreting the field value. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP's HTTP/1.1 decoder correctly parses field lines as `field-name ":" OWS field-value OWS`. Leading and trailing whitespace around field values is trimmed. Field names are treated case-insensitively. Obsolete line folding (obs-fold) is handled by replacing with SP octets. - -**Key Components:** -- `Http11ResponseDecoder` — header field parsing and extraction -- `Http11RequestEncoder` — header field serialization - -**Compliance Details:** -- ✅ Field-line format: `field-name ":" OWS field-value OWS` -- ✅ Whitespace between field-name and colon rejected (as client, not generated) -- ✅ Leading/trailing OWS trimmed from field values -- ✅ Obs-fold replaced with SP when encountered -- ✅ Case-insensitive field name handling - -**Gaps:** None identified - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/07_6_message_body.md b/notes/RFC/RFC9112/sections/07_6_message_body.md index bda67d4b3..def9efa39 100644 --- a/notes/RFC/RFC9112/sections/07_6_message_body.md +++ b/notes/RFC/RFC9112/sections/07_6_message_body.md @@ -1,4 +1,4 @@ ---- +--- title: 6. Message Body rfc_number: 9112 rfc_section: '6' @@ -288,36 +288,3 @@ tags: > **MUST NOT**: client MUST NOT process, cache, or forward such extra data as a separate response, since such behavior would be vulnerable to cache poisoning. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP implements the full message body length determination algorithm from §6.3. The decoder supports Transfer-Encoding (chunked), Content-Length, and connection-close body framing. Transfer-Encoding takes precedence over Content-Length when both are present. The client generates Content-Length for known-size bodies and chunked encoding for streaming bodies. - -**Key Components:** -- `Http11ResponseDecoder` — body length determination, chunked decoding, Content-Length framing -- `Http11RequestEncoder` — Content-Length and Transfer-Encoding generation -- `ChunkedDecodingStage` — Akka.Streams stage for chunked transfer decoding - -**Compliance Details:** -- ✅ Transfer-Encoding overrides Content-Length (§6.3 rule 3) -- ✅ Chunked transfer coding decoding (§6.3 rule 4) -- ✅ Content-Length body framing (§6.3 rule 6) -- ✅ Connection-close body termination (§6.3 rule 8) -- ✅ HEAD/1xx/204/304 responses have no body (§6.3 rule 1) -- ✅ Invalid Content-Length detection -- ✅ Client sends Content-Length or chunked for request bodies - -**Gaps:** -- CONNECT tunnel response handling (§6.3 rule 2) — CONNECT not supported - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/08_7_transfer_codings.md b/notes/RFC/RFC9112/sections/08_7_transfer_codings.md index 342b72e29..2846a36d7 100644 --- a/notes/RFC/RFC9112/sections/08_7_transfer_codings.md +++ b/notes/RFC/RFC9112/sections/08_7_transfer_codings.md @@ -1,4 +1,4 @@ ---- +--- title: 7. Transfer Codings rfc_number: 9112 rfc_section: '7' @@ -251,38 +251,3 @@ tags: Connection header field (Section 7.6.1 of [HTTP]) in order to prevent the TE header field from being forwarded by intermediaries that do not support its semantics. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP fully supports chunked transfer coding for both decoding responses and encoding requests. The `ChunkedDecodingStage` handles chunk-size parsing, chunk-data extraction, last-chunk detection, and trailer section processing. Chunk extensions are parsed and ignored per spec. Compression transfer codings (gzip, deflate) are handled by the separate `DecompressionStage`. - -**Key Components:** -- `ChunkedDecodingStage` — Akka.Streams stage for chunked transfer decoding -- `Http11ResponseDecoder` — Transfer-Encoding detection and routing -- `Http11RequestEncoder` — chunked encoding for streaming request bodies -- `DecompressionStage` — handles gzip/deflate transfer codings - -**Compliance Details:** -- ✅ Chunked transfer coding parsing and decoding (§7.1) -- ✅ Large chunk-size handling (overflow protection) -- ✅ Chunk extensions parsed and ignored (§7.1.1) -- ✅ Trailer section handling (§7.1.2) -- ✅ Decoding algorithm per §7.1.3 -- ✅ Gzip and deflate compression codings (§7.2) -- ✅ TE header not sent with "chunked" (§7.4) - -**Gaps:** -- Compress/x-compress (LZW) not supported -- Chunk extension parameters not treated as error (SHOULD) - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/09_8_handling_incomplete_messages.md b/notes/RFC/RFC9112/sections/09_8_handling_incomplete_messages.md index 321cea585..c0cdc785c 100644 --- a/notes/RFC/RFC9112/sections/09_8_handling_incomplete_messages.md +++ b/notes/RFC/RFC9112/sections/09_8_handling_incomplete_messages.md @@ -1,4 +1,4 @@ ---- +--- title: 8. Handling Incomplete Messages rfc_number: 9112 rfc_section: '8' @@ -46,32 +46,3 @@ tags: considered complete unless an error was indicated by the underlying connection (e.g., an "incomplete close" in TLS would leave the response incomplete, as described in Section 9.8). - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP correctly detects and records incomplete response messages. When a connection closes prematurely (before Content-Length bytes received or before chunked zero-chunk), the response is marked as incomplete. The decoder distinguishes between connection-close terminated responses (complete if headers intact) and prematurely truncated responses. - -**Key Components:** -- `Http11ResponseDecoder` — incomplete message detection -- `MessageCompleteness` — tracks whether full body was received -- `ConnectionPool` — handles connection failures and retries - -**Compliance Details:** -- ✅ Incomplete chunked messages detected (no zero-chunk received) -- ✅ Content-Length mismatch detected (fewer bytes than declared) -- ✅ Connection-close responses considered complete if headers intact -- ✅ TLS incomplete close detection - -**Gaps:** None identified - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/10_9_1_establishment.md b/notes/RFC/RFC9112/sections/10_9_1_establishment.md index 8f03622a4..ddeb672c2 100644 --- a/notes/RFC/RFC9112/sections/10_9_1_establishment.md +++ b/notes/RFC/RFC9112/sections/10_9_1_establishment.md @@ -1,4 +1,4 @@ ---- +--- title: "9.1. Establishment" rfc_number: 9112 rfc_section: "9.1" @@ -44,4 +44,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/11_9_2_associating_a_response_to_a_request.md b/notes/RFC/RFC9112/sections/11_9_2_associating_a_response_to_a_request.md index 4144f5c22..8eec5bfb6 100644 --- a/notes/RFC/RFC9112/sections/11_9_2_associating_a_response_to_a_request.md +++ b/notes/RFC/RFC9112/sections/11_9_2_associating_a_response_to_a_request.md @@ -1,4 +1,4 @@ ---- +--- title: "9.2. Associating a Response to a Request" rfc_number: 9112 rfc_section: "9.2" @@ -33,4 +33,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/12_9_3_persistence.md b/notes/RFC/RFC9112/sections/12_9_3_persistence.md index fa7de2a1d..7c67307a6 100644 --- a/notes/RFC/RFC9112/sections/12_9_3_persistence.md +++ b/notes/RFC/RFC9112/sections/12_9_3_persistence.md @@ -1,4 +1,4 @@ ---- +--- title: 9.3. Persistence rfc_number: 9112 rfc_section: '9.3' @@ -117,35 +117,3 @@ tags: > **SHOULD**: SHOULD forward any received responses and then close the corresponding outbound connection(s) so that the outbound user agent(s) can recover accordingly. - - ---- - -## TurboHTTP Compliance - -**Status:** ✅ Compliant - -**Implementation Notes:** -TurboHTTP fully supports HTTP/1.1 persistent connections. The connection pool maintains keep-alive connections and reuses them for subsequent requests. The `close` connection option is respected — connections are released when the server sends `Connection: close`. HTTP/1.0 keep-alive is also supported. The client reads the entire response body before reusing connections. - -**Key Components:** -- `ConnectionPool` — manages persistent connection lifecycle, keep-alive, and reuse -- `Http11ResponseDecoder` — detects `Connection: close` and keep-alive signals -- `RetryStage` — handles connection failures with automatic retry for idempotent methods - -**Compliance Details:** -- ✅ Persistent connections by default in HTTP/1.1 -- ✅ `Connection: close` option respected -- ✅ HTTP/1.0 keep-alive support -- ✅ Full response body consumed before connection reuse -- ✅ Connection retry for idempotent methods (§9.3.1) -- ⚠️ Pipelining not implemented (§9.3.2) — requests are serialized per connection - -**Gaps:** -- HTTP/1.1 pipelining not supported (sequential requests only) - -**Test References:** `TurboHTTP.Tests.RFC9112` - ---- - -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/13_9_4_concurrency.md b/notes/RFC/RFC9112/sections/13_9_4_concurrency.md index c72068f42..2cf86058d 100644 --- a/notes/RFC/RFC9112/sections/13_9_4_concurrency.md +++ b/notes/RFC/RFC9112/sections/13_9_4_concurrency.md @@ -1,4 +1,4 @@ ---- +--- title: "9.4. Concurrency" rfc_number: 9112 rfc_section: "9.4" @@ -39,4 +39,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/14_9_5_failures_and_timeouts.md b/notes/RFC/RFC9112/sections/14_9_5_failures_and_timeouts.md index 1bdaa447d..5d6c467b6 100644 --- a/notes/RFC/RFC9112/sections/14_9_5_failures_and_timeouts.md +++ b/notes/RFC/RFC9112/sections/14_9_5_failures_and_timeouts.md @@ -1,4 +1,4 @@ ---- +--- title: "9.5. Failures and Timeouts" rfc_number: 9112 rfc_section: "9.5" @@ -46,4 +46,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/15_9_6_tear-down.md b/notes/RFC/RFC9112/sections/15_9_6_tear-down.md index 4acfb233d..44e2abde6 100644 --- a/notes/RFC/RFC9112/sections/15_9_6_tear-down.md +++ b/notes/RFC/RFC9112/sections/15_9_6_tear-down.md @@ -1,4 +1,4 @@ ---- +--- title: "9.6. Tear-down" rfc_number: 9112 rfc_section: "9.6" @@ -79,4 +79,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/16_9_7_tls_connection_initiation.md b/notes/RFC/RFC9112/sections/16_9_7_tls_connection_initiation.md index ebc4b857f..c48843a40 100644 --- a/notes/RFC/RFC9112/sections/16_9_7_tls_connection_initiation.md +++ b/notes/RFC/RFC9112/sections/16_9_7_tls_connection_initiation.md @@ -1,4 +1,4 @@ ---- +--- title: "9.7. TLS Connection Initiation" rfc_number: 9112 rfc_section: "9.7" @@ -24,4 +24,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/17_9_8_tls_connection_closure.md b/notes/RFC/RFC9112/sections/17_9_8_tls_connection_closure.md index 82ae2d4e7..529695454 100644 --- a/notes/RFC/RFC9112/sections/17_9_8_tls_connection_closure.md +++ b/notes/RFC/RFC9112/sections/17_9_8_tls_connection_closure.md @@ -1,4 +1,4 @@ ---- +--- title: "9.8. TLS Connection Closure" rfc_number: 9112 rfc_section: "9.8" @@ -60,4 +60,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/18_10_enclosing_messages_as_data.md b/notes/RFC/RFC9112/sections/18_10_enclosing_messages_as_data.md index 7418b60ab..5a2d30a66 100644 --- a/notes/RFC/RFC9112/sections/18_10_enclosing_messages_as_data.md +++ b/notes/RFC/RFC9112/sections/18_10_enclosing_messages_as_data.md @@ -1,4 +1,4 @@ ---- +--- title: "10. Enclosing Messages as Data" rfc_number: 9112 rfc_section: "10" @@ -127,4 +127,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/19_11_security_considerations.md b/notes/RFC/RFC9112/sections/19_11_security_considerations.md index f7ed5580e..b80a24e51 100644 --- a/notes/RFC/RFC9112/sections/19_11_security_considerations.md +++ b/notes/RFC/RFC9112/sections/19_11_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "11. Security Considerations" rfc_number: 9112 rfc_section: "11" @@ -117,4 +117,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/20_12_iana_considerations.md b/notes/RFC/RFC9112/sections/20_12_iana_considerations.md index bad4357dc..9785152dc 100644 --- a/notes/RFC/RFC9112/sections/20_12_iana_considerations.md +++ b/notes/RFC/RFC9112/sections/20_12_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "12. IANA Considerations" rfc_number: 9112 rfc_section: "12" @@ -89,4 +89,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/86_13_references.md b/notes/RFC/RFC9112/sections/86_13_references.md index e95862c87..3c45da797 100644 --- a/notes/RFC/RFC9112/sections/86_13_references.md +++ b/notes/RFC/RFC9112/sections/86_13_references.md @@ -1,4 +1,4 @@ ---- +--- title: "13. References" rfc_number: 9112 rfc_section: "13" @@ -130,4 +130,3 @@ tags: [RFC9112, HTTP/1.1, message-framing, chunked-encoding, connection-manageme --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/91_appendix_a_collected_abnf.md b/notes/RFC/RFC9112/sections/91_appendix_a_collected_abnf.md index 550385936..33e298c33 100644 --- a/notes/RFC/RFC9112/sections/91_appendix_a_collected_abnf.md +++ b/notes/RFC/RFC9112/sections/91_appendix_a_collected_abnf.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Collected ABNF" rfc_number: 9112 rfc_section: "Appendix A" @@ -14,7 +14,6 @@ Appendix A. Collected ABNF In the collected ABNF below, list rules are expanded per Section 5.6.1 of [HTTP]. - ```abnf BWS = @@ -36,7 +35,6 @@ Appendix A. Collected ABNF ) ] - ```abnf absolute-URI = absolute-form = absolute-URI @@ -94,4 +92,3 @@ Appendix A. Collected ABNF --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/92_appendix_b_differences_between_http_and_mime.md b/notes/RFC/RFC9112/sections/92_appendix_b_differences_between_http_and_mime.md index 0b925e997..d2bc386e5 100644 --- a/notes/RFC/RFC9112/sections/92_appendix_b_differences_between_http_and_mime.md +++ b/notes/RFC/RFC9112/sections/92_appendix_b_differences_between_http_and_mime.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Differences between HTTP and MIME" rfc_number: 9112 rfc_section: "Appendix B" @@ -107,4 +107,3 @@ B.6. MHTML and Line Length Limitations --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/93_appendix_c_changes_from_previous_rfcs.md b/notes/RFC/RFC9112/sections/93_appendix_c_changes_from_previous_rfcs.md index 0b6be50d3..238ef88ce 100644 --- a/notes/RFC/RFC9112/sections/93_appendix_c_changes_from_previous_rfcs.md +++ b/notes/RFC/RFC9112/sections/93_appendix_c_changes_from_previous_rfcs.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix C. Changes from Previous RFCs" rfc_number: 9112 rfc_section: "Appendix C" @@ -118,4 +118,3 @@ C.3. Changes from RFC 7230 --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9112/sections/99_acknowledgements.md b/notes/RFC/RFC9112/sections/99_acknowledgements.md index 7aab268b9..e9a659efb 100644 --- a/notes/RFC/RFC9112/sections/99_acknowledgements.md +++ b/notes/RFC/RFC9112/sections/99_acknowledgements.md @@ -1,4 +1,4 @@ ---- +--- title: "Acknowledgements" rfc_number: 9112 rfc_section: "-" @@ -16,4 +16,3 @@ Acknowledgements --- -**Navigation:** [[../RFC9112|RFC9112 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/RFC9113.md b/notes/RFC/RFC9113/RFC9113.md index d018629df..673383570 100644 --- a/notes/RFC/RFC9113/RFC9113.md +++ b/notes/RFC/RFC9113/RFC9113.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9113 — HTTP/2" rfc_number: 9113 description: "HTTP/2 binary framing protocol. Defines frame types, stream states, multiplexing, flow control, HPACK integration, and connection preface." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9113" **Official RFC**: [RFC 9113](https://www.rfc-editor.org/rfc/rfc9113) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 87/100 | -| **Implementation Status** | ✅ Complete | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9113/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9113/` — 27 files, 545 tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9113/` | -| **Key Gaps** | MAX_CONCURRENT_STREAMS enforcement, SETTINGS acknowledgment tracking, stream priority routing | - ## Core Concepts - [[RFC9113/sections/05_4_http_frames|§4 HTTP Frames]] — 9-byte frame header (length + type + flags + stream ID) @@ -32,55 +21,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9113" - [[RFC9113/sections/18_6_8_goaway|§6.8 GOAWAY]] — graceful connection shutdown - [[RFC9113/sections/22_8_1_http_message_framing|§8.1 Message Framing]] — mapping HTTP messages to frames -## Implementation Notes - -### Encoder - -| Component | File | Purpose | -|-----------|------|---------| -| `Http2RequestEncoder` | `Protocol/RFC9113/Http2RequestEncoder.cs` | Request → HEADERS/DATA frame encoding | - -### Decoder - -| Component | File | Purpose | -|-----------|------|---------| -| `Http2DecoderPipeline` | `Protocol/RFC9113/Http2DecoderPipeline.cs` | Frame parsing with stream demultiplexing | -| `Http2EventAggregator` | `Protocol/RFC9113/Http2EventAggregator.cs` | Frame events → HttpResponseMessage assembly | -| `Http2CompletionDecoder` | `Protocol/RFC9113/Http2CompletionDecoder.cs` | Convenience wrapper for complete response decoding | - -### Frame Types - -| Frame | Type ID | File | -|-------|---------|------| -| `DataFrame` | 0x0 | `Protocol/RFC9113/Http2Frame.cs` | -| `HeadersFrame` | 0x1 | `Protocol/RFC9113/Http2Frame.cs` | -| `RstStreamFrame` | 0x3 | `Protocol/RFC9113/Http2Frame.cs` | -| `SettingsFrame` | 0x4 | `Protocol/RFC9113/Http2Frame.cs` | -| `PingFrame` | 0x6 | `Protocol/RFC9113/Http2Frame.cs` | -| `GoAwayFrame` | 0x7 | `Protocol/RFC9113/Http2Frame.cs` | -| `WindowUpdateFrame` | 0x8 | `Protocol/RFC9113/Http2Frame.cs` | -| `ContinuationFrame` | 0x9 | `Protocol/RFC9113/Http2Frame.cs` | - -### Stages - -| Stage | File | Purpose | -|-------|------|---------| -| `Http20EncoderStage` | `Streams/Stages/Encoding/Http20EncoderStage.cs` | Request encoding for HTTP/2 pipeline | -| `Http20DecoderStage` | `Streams/Stages/Decoding/Http20DecoderStage.cs` | Frame decoding in pipeline | -| `Http20ConnectionStage` | `Streams/Stages/Decoding/Http20ConnectionStage.cs` | Connection-level frame handling (SETTINGS/PING/GOAWAY) | -| `Http20StreamStage` | `Streams/Stages/Decoding/Http20StreamStage.cs` | Frame → HttpResponseMessage assembly | -| `Http20CorrelationStage` | `Streams/Stages/Routing/Http20CorrelationStage.cs` | Stream-ID-based request-response matching | -| `StreamIdAllocatorStage` | `Streams/Stages/Routing/StreamIdAllocatorStage.cs` | Client stream ID allocation (1, 3, 5, …) | -| `Request2FrameStage` | `Streams/Stages/Encoding/Request2FrameStage.cs` | Request → HTTP/2 frame conversion | -| `PrependPrefaceStage` | `Streams/Stages/Encoding/PrependPrefaceStage.cs` | HTTP/2 connection preface | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/RFC9113/` | 545 unit tests — frames, streams, flow control, HPACK, pseudo-headers | -| `TurboHTTP.StreamTests/RFC9113/` | Stage behaviour tests — encoder, decoder, connection, stream, correlation stages | - ## Sections | # | Section | File | Status | @@ -134,7 +74,7 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9113" - [[RFC7541/RFC7541|RFC 7541 — HPACK]] — header compression for HTTP/2 - [[RFC9110/RFC9110|RFC 9110 — HTTP Semantics]] — shared semantics - [[RFC9114/RFC9114|RFC 9114 — HTTP/3]] — QUIC-based successor -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking +- [[RFC7838/RFC7838|RFC 7838 — Alt-Svc]] — HTTP Alternative Services; defines the ALTSVC frame used in HTTP/2 --- diff --git a/notes/RFC/RFC9113/sections/00_preamble.md b/notes/RFC/RFC9113/sections/00_preamble.md index 5d304ce7e..c6e77e5be 100644 --- a/notes/RFC/RFC9113/sections/00_preamble.md +++ b/notes/RFC/RFC9113/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9113 rfc_section: "preamble" @@ -9,17 +9,12 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET ## Preamble - - - - Internet Engineering Task Force (IETF) M. Thomson, Ed. Request for Comments: 9113 Mozilla Obsoletes: 7540, 8740 C. Benfield, Ed. Category: Standards Track Apple Inc. ISSN: 2070-1721 June 2022 - HTTP/2 Abstract @@ -166,4 +161,3 @@ Table of Contents --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/02_1_introduction.md b/notes/RFC/RFC9113/sections/02_1_introduction.md index befe4f6aa..071a1cb0e 100644 --- a/notes/RFC/RFC9113/sections/02_1_introduction.md +++ b/notes/RFC/RFC9113/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9113 rfc_section: "1" @@ -52,4 +52,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/03_2_http2_protocol_overview.md b/notes/RFC/RFC9113/sections/03_2_http2_protocol_overview.md index c3aed4083..690e456ef 100644 --- a/notes/RFC/RFC9113/sections/03_2_http2_protocol_overview.md +++ b/notes/RFC/RFC9113/sections/03_2_http2_protocol_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "2. HTTP/2 Protocol Overview" rfc_number: 9113 rfc_section: "2" @@ -135,4 +135,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/04_3_starting_http2.md b/notes/RFC/RFC9113/sections/04_3_starting_http2.md index 0238d0dad..5a821ee94 100644 --- a/notes/RFC/RFC9113/sections/04_3_starting_http2.md +++ b/notes/RFC/RFC9113/sections/04_3_starting_http2.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Starting HTTP/2" rfc_number: 9113 rfc_section: "3" @@ -130,4 +130,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/05_4_http_frames.md b/notes/RFC/RFC9113/sections/05_4_http_frames.md index a669c237f..c58ac7b4e 100644 --- a/notes/RFC/RFC9113/sections/05_4_http_frames.md +++ b/notes/RFC/RFC9113/sections/05_4_http_frames.md @@ -204,25 +204,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET | the connection preface to reduce the value below the initial | value of 4,096 is somewhat better supported, but this might | fail with some implementations. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`Http2FrameDecoder.cs`** — Parses the 9-octet frame header per §4.1; validates SETTINGS_MAX_FRAME_SIZE limits per §4.2; raises FRAME_SIZE_ERROR for oversized frames -- **`Http2FrameEncoder.cs`** — Encodes all 10 defined frame types with correct type codes and flag handling -- **`HpackDecoder.cs`** — Full HPACK decompression per §4.3 with dynamic table state management -- **`HpackEncoder.cs`** — HPACK compression with static/dynamic table support - -### Test References -- 482 total tests across 27 test files for RFC9113 - -### Known Gaps -- None - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/06_5_1_stream_states.md b/notes/RFC/RFC9113/sections/06_5_1_stream_states.md index 5af138eaf..08eb303b3 100644 --- a/notes/RFC/RFC9113/sections/06_5_1_stream_states.md +++ b/notes/RFC/RFC9113/sections/06_5_1_stream_states.md @@ -1,4 +1,4 @@ ---- +--- title: "5.1. Stream States" rfc_number: 9113 rfc_section: "5.1" @@ -341,27 +341,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`Http2StreamStateMachine.cs`** — Implements the full stream state machine (idle → open → half-closed → closed) per §5.1 Figure 2; validates state transitions and raises `PROTOCOL_ERROR` or `STREAM_CLOSED` for invalid transitions -- **`Http2StreamManager.cs`** — Manages concurrent stream tracking; enforces `SETTINGS_MAX_CONCURRENT_STREAMS` per §5.1.2; assigns odd-numbered stream IDs for client-initiated streams per §5.1.1 -- **`Http2Connection.cs`** — Coordinates stream lifecycle across the connection; handles RST_STREAM and END_STREAM flag processing for state transitions - -### Test References - -- `TurboHTTP.Tests/RFC9113/05_Http2StreamStateTests.cs` — Stream state machine transitions, invalid state detection -- `TurboHTTP.Tests/RFC9113/06_Http2StreamIdTests.cs` — Stream identifier ordering, odd/even validation -- `TurboHTTP.Tests/RFC9113/07_Http2ConcurrencyTests.cs` — `SETTINGS_MAX_CONCURRENT_STREAMS` enforcement - -### Known Gaps - -- ⚠️ `SETTINGS_MAX_CONCURRENT_STREAMS` enforcement — tracked but not actively enforced as a hard limit when the server hasn't advertised a value (initial value is unlimited per spec) -- ❌ Reserved stream states (§5.1 reserved local/remote) — not fully implemented since server push (`PUSH_PROMISE`) is not supported - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/07_5_2_flow_control.md b/notes/RFC/RFC9113/sections/07_5_2_flow_control.md index a32bc9aac..277fb03a5 100644 --- a/notes/RFC/RFC9113/sections/07_5_2_flow_control.md +++ b/notes/RFC/RFC9113/sections/07_5_2_flow_control.md @@ -1,4 +1,4 @@ ---- +--- title: "5.2. Flow Control" rfc_number: 9113 rfc_section: "5.2" @@ -112,29 +112,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET with the need to manage resource exhaustion risks and should take careful note of Section 10.5 in defining their strategy to manage window sizes. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`Http2FlowController.cs`** — Implements credit-based flow control per §5.2.1; tracks both stream-level and connection-level windows; initial window size 65,535 octets per §5.2.1 principle 4; only DATA frames consume flow-control credit per principle 5 -- **`Http2WindowUpdateHandler.cs`** — Processes WINDOW_UPDATE frames to increase flow-control windows; raises `FLOW_CONTROL_ERROR` when window exceeds 2^31-1 -- **`Http2Connection.cs`** — Reads and processes frames from TCP buffer promptly per §5.2.2 to prevent deadlock on WINDOW_UPDATE frames - -### Test References - -- `TurboHTTP.Tests/RFC9113/08_Http2FlowControlTests.cs` — Window tracking, credit consumption, overflow detection -- `TurboHTTP.Tests/RFC9113/09_Http2WindowUpdateTests.cs` — WINDOW_UPDATE processing, connection vs stream windows -- `TurboHTTP.StreamTests/` — End-to-end flow control under backpressure - -### Known Gaps - -- ⚠️ Adaptive window sizing — uses fixed window management rather than bandwidth*delay product-aware algorithm per §5.2.3; functional but may not achieve optimal throughput on high-latency connections - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/08_5_3_prioritization.md b/notes/RFC/RFC9113/sections/08_5_3_prioritization.md index d53faf087..88e1690db 100644 --- a/notes/RFC/RFC9113/sections/08_5_3_prioritization.md +++ b/notes/RFC/RFC9113/sections/08_5_3_prioritization.md @@ -1,4 +1,4 @@ ---- +--- title: "5.3. Prioritization" rfc_number: 9113 rfc_section: "5.3" @@ -75,4 +75,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/09_5_4_error_handling.md b/notes/RFC/RFC9113/sections/09_5_4_error_handling.md index e8bccc38f..0deb1afa6 100644 --- a/notes/RFC/RFC9113/sections/09_5_4_error_handling.md +++ b/notes/RFC/RFC9113/sections/09_5_4_error_handling.md @@ -1,4 +1,4 @@ ---- +--- title: "5.4. Error Handling" rfc_number: 9113 rfc_section: "5.4" @@ -96,4 +96,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/10_5_5_extending_http2.md b/notes/RFC/RFC9113/sections/10_5_5_extending_http2.md index 9c79103fb..2dc714657 100644 --- a/notes/RFC/RFC9113/sections/10_5_5_extending_http2.md +++ b/notes/RFC/RFC9113/sections/10_5_5_extending_http2.md @@ -1,4 +1,4 @@ ---- +--- title: "5.5. Extending HTTP/2" rfc_number: 9113 rfc_section: "5.5" @@ -62,4 +62,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/11_6_1_data.md b/notes/RFC/RFC9113/sections/11_6_1_data.md index 6c99870f9..f10962f91 100644 --- a/notes/RFC/RFC9113/sections/11_6_1_data.md +++ b/notes/RFC/RFC9113/sections/11_6_1_data.md @@ -1,4 +1,4 @@ ---- +--- title: "6.1. DATA" rfc_number: 9113 rfc_section: "6.1" @@ -110,4 +110,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/12_6_2_headers.md b/notes/RFC/RFC9113/sections/12_6_2_headers.md index 896c7d348..f432ef506 100644 --- a/notes/RFC/RFC9113/sections/12_6_2_headers.md +++ b/notes/RFC/RFC9113/sections/12_6_2_headers.md @@ -1,4 +1,4 @@ ---- +--- title: "6.2. HEADERS" rfc_number: 9113 rfc_section: "6.2" @@ -116,4 +116,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/13_6_3_priority.md b/notes/RFC/RFC9113/sections/13_6_3_priority.md index 05bb14352..6bd0951d0 100644 --- a/notes/RFC/RFC9113/sections/13_6_3_priority.md +++ b/notes/RFC/RFC9113/sections/13_6_3_priority.md @@ -1,4 +1,4 @@ ---- +--- title: "6.3. PRIORITY" rfc_number: 9113 rfc_section: "6.3" @@ -59,4 +59,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/14_6_4_rst_stream.md b/notes/RFC/RFC9113/sections/14_6_4_rst_stream.md index 9fdabc27d..494949509 100644 --- a/notes/RFC/RFC9113/sections/14_6_4_rst_stream.md +++ b/notes/RFC/RFC9113/sections/14_6_4_rst_stream.md @@ -1,4 +1,4 @@ ---- +--- title: "6.4. RST_STREAM" rfc_number: 9113 rfc_section: "6.4" @@ -60,4 +60,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/15_6_5_settings.md b/notes/RFC/RFC9113/sections/15_6_5_settings.md index 3c3f16eb0..4d9dd270f 100644 --- a/notes/RFC/RFC9113/sections/15_6_5_settings.md +++ b/notes/RFC/RFC9113/sections/15_6_5_settings.md @@ -1,4 +1,4 @@ ---- +--- title: "6.5. SETTINGS" rfc_number: 9113 rfc_section: "6.5" @@ -197,31 +197,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET allowance needs to be made for processing delays at the peer; a timeout that is solely based on the round-trip time between endpoints might result in spurious errors. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes - -- **`Http2Settings.cs`** — Supports all 6 defined settings: `SETTINGS_HEADER_TABLE_SIZE` (0x01), `SETTINGS_ENABLE_PUSH` (0x02), `SETTINGS_MAX_CONCURRENT_STREAMS` (0x03), `SETTINGS_INITIAL_WINDOW_SIZE` (0x04), `SETTINGS_MAX_FRAME_SIZE` (0x05), `SETTINGS_MAX_HEADER_LIST_SIZE` (0x06) -- **`Http2FrameDecoder.cs`** — Validates SETTINGS frame: stream ID must be 0, length must be multiple of 6, ACK frame must be empty per §6.5 -- **`Http2Connection.cs`** — Sends SETTINGS at connection start per §6.5; processes settings in order per §6.5.3; sends ACK after applying received settings -- **`Http2SettingsValidator.cs`** — Validates setting values: `SETTINGS_ENABLE_PUSH` must be 0 or 1, `SETTINGS_INITIAL_WINDOW_SIZE` ≤ 2^31-1, `SETTINGS_MAX_FRAME_SIZE` between 2^14 and 2^24-1 - -### Test References - -- `TurboHTTP.Tests/RFC9113/10_Http2SettingsTests.cs` — Settings encoding/decoding, value validation -- `TurboHTTP.Tests/RFC9113/11_Http2SettingsAckTests.cs` — ACK synchronization, timeout handling -- `TurboHTTP.Tests/RFC9113/12_Http2SettingsErrorTests.cs` — Invalid settings detection (bad stream ID, wrong length, invalid values) - -### Known Gaps - -- ⚠️ SETTINGS ACK timeout (§6.5.3) — no `SETTINGS_TIMEOUT` error is raised if peer doesn't acknowledge within reasonable time; relies on connection-level timeout instead -- ⚠️ `SETTINGS_ENABLE_PUSH` — always sent as 0 (push disabled) but server's push-related frames are not fully validated against this setting - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/16_6_6_push_promise.md b/notes/RFC/RFC9113/sections/16_6_6_push_promise.md index 81dd6cdf3..8b8be9281 100644 --- a/notes/RFC/RFC9113/sections/16_6_6_push_promise.md +++ b/notes/RFC/RFC9113/sections/16_6_6_push_promise.md @@ -1,4 +1,4 @@ ---- +--- title: "6.6. PUSH_PROMISE" rfc_number: 9113 rfc_section: "6.6" @@ -132,4 +132,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/17_6_7_ping.md b/notes/RFC/RFC9113/sections/17_6_7_ping.md index cb2b75623..1c5875726 100644 --- a/notes/RFC/RFC9113/sections/17_6_7_ping.md +++ b/notes/RFC/RFC9113/sections/17_6_7_ping.md @@ -1,4 +1,4 @@ ---- +--- title: "6.7. PING" rfc_number: 9113 rfc_section: "6.7" @@ -61,4 +61,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/18_6_8_goaway.md b/notes/RFC/RFC9113/sections/18_6_8_goaway.md index 508745699..24db4d9db 100644 --- a/notes/RFC/RFC9113/sections/18_6_8_goaway.md +++ b/notes/RFC/RFC9113/sections/18_6_8_goaway.md @@ -1,4 +1,4 @@ ---- +--- title: "6.8. GOAWAY" rfc_number: 9113 rfc_section: "6.8" @@ -152,4 +152,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/19_6_9_window_update.md b/notes/RFC/RFC9113/sections/19_6_9_window_update.md index cebc974c9..f8273d776 100644 --- a/notes/RFC/RFC9113/sections/19_6_9_window_update.md +++ b/notes/RFC/RFC9113/sections/19_6_9_window_update.md @@ -1,4 +1,4 @@ ---- +--- title: "6.9. WINDOW_UPDATE" rfc_number: 9113 rfc_section: "6.9" @@ -192,4 +192,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/20_6_10_continuation.md b/notes/RFC/RFC9113/sections/20_6_10_continuation.md index 0cb4ce0fc..97c4040ff 100644 --- a/notes/RFC/RFC9113/sections/20_6_10_continuation.md +++ b/notes/RFC/RFC9113/sections/20_6_10_continuation.md @@ -1,4 +1,4 @@ ---- +--- title: "6.10. CONTINUATION" rfc_number: 9113 rfc_section: "6.10" @@ -62,4 +62,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/21_7_error_codes.md b/notes/RFC/RFC9113/sections/21_7_error_codes.md index a37c4039e..958f6cb0c 100644 --- a/notes/RFC/RFC9113/sections/21_7_error_codes.md +++ b/notes/RFC/RFC9113/sections/21_7_error_codes.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Error Codes" rfc_number: 9113 rfc_section: "7" @@ -69,26 +69,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET > **MUST NOT**: Unknown or unsupported error codes MUST NOT trigger any special behavior. These MAY be treated by an implementation as being equivalent to INTERNAL_ERROR. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`Http2ErrorCode.cs`** — Enum defining all 14 error codes (0x00–0x0d) matching RFC definitions exactly -- **`Http2FrameDecoder.cs`** — Maps received error codes in RST_STREAM/GOAWAY frames to typed error handling -- **`Http2FrameEncoder.cs`** — Sends correct error codes in RST_STREAM and GOAWAY frames -- **`Http2ConnectionStage.cs`** — Generates appropriate error codes for protocol violations (PROTOCOL_ERROR, FLOW_CONTROL_ERROR, FRAME_SIZE_ERROR, COMPRESSION_ERROR) - -### Test References -- `TurboHTTP.Tests/RFC9113/21_Http2ErrorCodeTests.cs` — Error code propagation and handling tests - -### Known Gaps -- ⚠️ ENHANCE_YOUR_CALM (0x0b) — Not actively sent; no rate-limiting detection implemented -- ⚠️ HTTP_1_1_REQUIRED (0x0d) — Not sent; no protocol downgrade mechanism implemented - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/22_8_1_http_message_framing.md b/notes/RFC/RFC9113/sections/22_8_1_http_message_framing.md index 332f725db..32f4c4434 100644 --- a/notes/RFC/RFC9113/sections/22_8_1_http_message_framing.md +++ b/notes/RFC/RFC9113/sections/22_8_1_http_message_framing.md @@ -1,4 +1,4 @@ ---- +--- title: "8.1. HTTP Message Framing" rfc_number: 9113 rfc_section: "8.1" @@ -127,26 +127,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET These requirements are intended to protect against several types of common attacks against HTTP; they are deliberately strict because being permissive can expose implementations to these vulnerabilities. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`Http2FrameDecoder.cs`** — Validates message framing: HEADERS→CONTINUATION sequences, END_STREAM/END_HEADERS flag handling, content-length vs DATA payload length checks -- **`Http2FrameEncoder.cs`** — Produces correct HEADERS/DATA/CONTINUATION sequences with proper flag management -- **`Http2StreamState.cs`** — Tracks stream lifecycle (open → half-closed → closed) per §8.1 framing rules -- **`Http2ConnectionStage.cs`** — Detects and rejects malformed messages per §8.1.1; sends PROTOCOL_ERROR stream errors for violations - -### Test References -- `TurboHTTP.Tests/RFC9113/22_Http2MessageFramingTests.cs` — Message structure, END_STREAM handling, malformed message detection - -### Known Gaps -- ⚠️ Trailer field pseudo-header rejection — Trailers with pseudo-headers detected but error response generation is basic -- ❌ Intermediary forwarding rules — TurboHTTP is a client library, not an intermediary; forwarding checks not applicable - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/23_8_2_http_fields.md b/notes/RFC/RFC9113/sections/23_8_2_http_fields.md index 7fec94ec1..1cb579379 100644 --- a/notes/RFC/RFC9113/sections/23_8_2_http_fields.md +++ b/notes/RFC/RFC9113/sections/23_8_2_http_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "8.2. HTTP Fields" rfc_number: 9113 rfc_section: "8.2" @@ -124,25 +124,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET cookie: a=b cookie: c=d cookie: e=f - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes -- **`HpackEncoder.cs`** — Converts field names to lowercase per §8.2; applies Cookie splitting for compression efficiency per §8.2.3 -- **`HpackDecoder.cs`** — Validates field name/value character ranges per §8.2.1; rejects prohibited characters (NUL, CR, LF in values; uppercase/non-visible in names) -- **`Http2FrameDecoder.cs`** — Strips connection-specific headers per §8.2.2 (Connection, Keep-Alive, Transfer-Encoding, Upgrade, Proxy-Connection) -- **`Http2RequestEncoder.cs`** — Ensures TE header only contains "trailers" value when present - -### Test References -- `TurboHTTP.Tests/RFC9113/23_Http2FieldTests.cs` — Field validation, connection-specific header rejection, Cookie compression - -### Known Gaps -- ⚠️ Cookie reconstitution — Split Cookie headers are concatenated on decode but edge cases with malformed cookie-pairs may not be fully covered - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/24_8_3_http_control_data.md b/notes/RFC/RFC9113/sections/24_8_3_http_control_data.md index 8e0529019..eeaa4a5a2 100644 --- a/notes/RFC/RFC9113/sections/24_8_3_http_control_data.md +++ b/notes/RFC/RFC9113/sections/24_8_3_http_control_data.md @@ -1,4 +1,4 @@ ---- +--- title: "8.3. HTTP Control Data" rfc_number: 9113 rfc_section: "8.3" @@ -140,4 +140,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/25_8_4_server_push.md b/notes/RFC/RFC9113/sections/25_8_4_server_push.md index c55c131b8..329e539ac 100644 --- a/notes/RFC/RFC9113/sections/25_8_4_server_push.md +++ b/notes/RFC/RFC9113/sections/25_8_4_server_push.md @@ -1,4 +1,4 @@ ---- +--- title: "8.4. Server Push" rfc_number: 9113 rfc_section: "8.4" @@ -176,4 +176,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/26_8_5_the_connect_method.md b/notes/RFC/RFC9113/sections/26_8_5_the_connect_method.md index 10c1ba25f..65639e828 100644 --- a/notes/RFC/RFC9113/sections/26_8_5_the_connect_method.md +++ b/notes/RFC/RFC9113/sections/26_8_5_the_connect_method.md @@ -1,4 +1,4 @@ ---- +--- title: "8.5. The CONNECT Method" rfc_number: 9113 rfc_section: "8.5" @@ -67,4 +67,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/27_8_6_the_upgrade_header_field.md b/notes/RFC/RFC9113/sections/27_8_6_the_upgrade_header_field.md index 5ad07e132..c54cb06dc 100644 --- a/notes/RFC/RFC9113/sections/27_8_6_the_upgrade_header_field.md +++ b/notes/RFC/RFC9113/sections/27_8_6_the_upgrade_header_field.md @@ -1,4 +1,4 @@ ---- +--- title: "8.6. The Upgrade Header Field" rfc_number: 9113 rfc_section: "8.6" @@ -22,4 +22,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/28_8_7_request_reliability.md b/notes/RFC/RFC9113/sections/28_8_7_request_reliability.md index 2436dfd3c..23e85a2ba 100644 --- a/notes/RFC/RFC9113/sections/28_8_7_request_reliability.md +++ b/notes/RFC/RFC9113/sections/28_8_7_request_reliability.md @@ -1,4 +1,4 @@ ---- +--- title: "8.7. Request Reliability" rfc_number: 9113 rfc_section: "8.7" @@ -49,4 +49,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/29_8_8_examples.md b/notes/RFC/RFC9113/sections/29_8_8_examples.md index d3106407e..c356da982 100644 --- a/notes/RFC/RFC9113/sections/29_8_8_examples.md +++ b/notes/RFC/RFC9113/sections/29_8_8_examples.md @@ -1,4 +1,4 @@ ---- +--- title: "8.8. Examples" rfc_number: 9113 rfc_section: "8.8" @@ -36,7 +36,6 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET accept = image/jpeg ``` - ### 8.8.2 Simple Response Similarly, a response that includes only control data and a response @@ -54,7 +53,6 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET expires = Thu, 23 Jan ... ``` - ### 8.8.3 Complex Request An HTTP POST request that includes control data and a request header @@ -81,7 +79,6 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET content-length = 123 ``` - DATA + END_STREAM {binary data} @@ -108,7 +105,6 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET content-length = 123 ``` - DATA + END_STREAM {binary data} @@ -137,7 +133,6 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET extension-field = bar ``` - HTTP/1.1 200 OK HEADERS Content-Type: image/jpeg ==> - END_STREAM Transfer-Encoding: chunked + END_HEADERS @@ -163,4 +158,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/30_9_http2_connections.md b/notes/RFC/RFC9113/sections/30_9_http2_connections.md index 4de257901..35db3274c 100644 --- a/notes/RFC/RFC9113/sections/30_9_http2_connections.md +++ b/notes/RFC/RFC9113/sections/30_9_http2_connections.md @@ -1,4 +1,4 @@ ---- +--- title: "9. HTTP/2 Connections" rfc_number: 9113 rfc_section: "9" @@ -197,28 +197,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET > **MAY**: TLS early data MAY be used to send requests, provided that the guidance in [RFC8470] is observed. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes -- **`Http2ConnectionPool.cs`** — Manages persistent connections per §9.1; limits to single connection per host:port pair; supports connection replacement on stream ID exhaustion -- **`Http2ConnectionStage.cs`** — Sends GOAWAY on graceful shutdown per §9.1; handles 421 Misdirected Request for connection reuse -- **`TlsHelper.cs`** — TLS 1.2+ required per §9.2; SNI extension always sent; TLS compression disabled; renegotiation rejected with PROTOCOL_ERROR - -### Test References -- `TurboHTTP.Tests/RFC9113/30_Http2ConnectionTests.cs` — Connection lifecycle, reuse, TLS requirements - -### Known Gaps -- ❌ TLS 1.2 cipher suite enforcement — Prohibited cipher suite list (Appendix A) not actively validated; relies on .NET runtime TLS defaults -- ❌ Post-handshake authentication rejection — TLS 1.3 CertificateRequest detection per §9.2.3 not explicitly implemented -- ⚠️ Ephemeral key size validation — DHE/ECDHE minimum key sizes not explicitly checked; delegated to .NET SslStream -- ⚠️ Early data (0-RTT) — Not supported; requests always sent after full handshake Clients send requests in early - data assuming initial values for all server settings. - ---- - -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/31_10_security_considerations.md b/notes/RFC/RFC9113/sections/31_10_security_considerations.md index 913dfdeba..51cde3690 100644 --- a/notes/RFC/RFC9113/sections/31_10_security_considerations.md +++ b/notes/RFC/RFC9113/sections/31_10_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "10. Security Considerations" rfc_number: 9113 rfc_section: "10" @@ -308,4 +308,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/32_11_iana_considerations.md b/notes/RFC/RFC9113/sections/32_11_iana_considerations.md index 1e4c0c988..3e95762d7 100644 --- a/notes/RFC/RFC9113/sections/32_11_iana_considerations.md +++ b/notes/RFC/RFC9113/sections/32_11_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "11. IANA Considerations" rfc_number: 9113 rfc_section: "11" @@ -65,4 +65,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/86_12_references.md b/notes/RFC/RFC9113/sections/86_12_references.md index e840788dc..a39789abe 100644 --- a/notes/RFC/RFC9113/sections/86_12_references.md +++ b/notes/RFC/RFC9113/sections/86_12_references.md @@ -1,4 +1,4 @@ ---- +--- title: "12. References" rfc_number: 9113 rfc_section: "12" @@ -177,4 +177,3 @@ tags: [RFC9113, HTTP/2, binary-framing, streams, multiplexing, flow-control, SET --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/91_appendix_a_prohibited_tls_12_cipher_suites.md b/notes/RFC/RFC9113/sections/91_appendix_a_prohibited_tls_12_cipher_suites.md index e9708219d..beece82ce 100644 --- a/notes/RFC/RFC9113/sections/91_appendix_a_prohibited_tls_12_cipher_suites.md +++ b/notes/RFC/RFC9113/sections/91_appendix_a_prohibited_tls_12_cipher_suites.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Prohibited TLS 1.2 Cipher Suites" rfc_number: 9113 rfc_section: "Appendix A" @@ -304,4 +304,3 @@ Appendix A. Prohibited TLS 1.2 Cipher Suites --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9113/sections/92_appendix_b_changes_from_rfc_7540.md b/notes/RFC/RFC9113/sections/92_appendix_b_changes_from_rfc_7540.md index 050cac3b3..5c891371b 100644 --- a/notes/RFC/RFC9113/sections/92_appendix_b_changes_from_rfc_7540.md +++ b/notes/RFC/RFC9113/sections/92_appendix_b_changes_from_rfc_7540.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Changes from RFC 7540" rfc_number: 9113 rfc_section: "Appendix B" @@ -65,4 +65,3 @@ Contributors --- -**Navigation:** [[../RFC9113|RFC9113 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/RFC9114.md b/notes/RFC/RFC9114/RFC9114.md index 91bd01695..da5fdc0a3 100644 --- a/notes/RFC/RFC9114/RFC9114.md +++ b/notes/RFC/RFC9114/RFC9114.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9114 — HTTP/3" rfc_number: 9114 description: "HTTP/3 protocol over QUIC transport. Defines variable-length frame headers, frame types, unidirectional stream types, QPACK integration, and connection shutdown." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9114" **Official RFC**: [RFC 9114](https://www.rfc-editor.org/rfc/rfc9114) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 60/100 | -| **Implementation Status** | 🔶 Partial | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9114/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9114/` — 32 files | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9114/` | -| **Key Gaps** | Server push acceptance, datagram extension, CANCEL_PUSH handling, detailed error codes | - ## Core Concepts - [[RFC9114/sections/03_2_http3_protocol_overview|§2 HTTP/3 Protocol Overview]] — QUIC-based HTTP mapping @@ -32,49 +21,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9114" - [[RFC9114/sections/11_5_connection_closure|§5 Connection Closure]] — graceful and immediate shutdown - [[RFC9114/sections/15_8_error_handling|§8 Error Handling]] — HTTP/3 error codes -## Implementation Notes - -### Encoder / Decoder - -| Component | File | Purpose | -|-----------|------|---------| -| `Http3FrameEncoder` | `Protocol/RFC9114/Http3FrameEncoder.cs` | Frame serialization | -| `Http3FrameDecoder` | `Protocol/RFC9114/Http3FrameDecoder.cs` | Frame parsing | -| `Http3RequestEncoder` | `Protocol/RFC9114/Http3RequestEncoder.cs` | Request → frame encoding | -| `Http3ResponseDecoder` | `Protocol/RFC9114/Http3ResponseDecoder.cs` | Frame → response decoding | - -### Stream Types - -| Component | File | Purpose | -|-----------|------|---------| -| `Http3ControlStream` | `Protocol/RFC9114/Http3ControlStream.cs` | Control stream management | -| `Http3RequestStream` | `Protocol/RFC9114/Http3RequestStream.cs` | Request stream handling | -| `Http3UniStream` | `Protocol/RFC9114/Http3UniStream.cs` | Unidirectional stream types | - -### Connection Management - -| Component | File | Purpose | -|-----------|------|---------| -| `Http3GoAwayHandler` | `Protocol/RFC9114/Http3GoAwayHandler.cs` | Graceful shutdown | -| `Http3IdleTimeoutHandler` | `Protocol/RFC9114/Http3IdleTimeoutHandler.cs` | Idle timeout management | -| `Http3Settings` | `Protocol/RFC9114/Http3Settings.cs` | Connection settings | - -### Stages - -| Stage | File | Purpose | -|-------|------|---------| -| `Http30EncoderStage` | `Streams/Stages/Encoding/Http30EncoderStage.cs` | Request encoding for HTTP/3 pipeline | -| `Http30DecoderStage` | `Streams/Stages/Decoding/Http30DecoderStage.cs` | Frame decoding in pipeline | -| `Http30ConnectionStage` | `Streams/Stages/Decoding/Http30ConnectionStage.cs` | Connection-level frame handling | -| `Http30StreamStage` | `Streams/Stages/Decoding/Http30StreamStage.cs` | Frame → response assembly | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/RFC9114/` | 32 test files — frames, streams, settings, validation | -| `TurboHTTP.StreamTests/RFC9114/` | Stage behaviour tests — encoder, decoder, connection, stream stages | - ## Sections | # | Section | File | Status | @@ -116,7 +62,7 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9114" - [[RFC9204/RFC9204|RFC 9204 — QPACK]] — header compression for HTTP/3 - [[RFC9110/RFC9110|RFC 9110 — HTTP Semantics]] — shared semantics - [[RFC9113/RFC9113|RFC 9113 — HTTP/2]] — predecessor binary protocol -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking +- [[RFC7838/RFC7838|RFC 7838 — Alt-Svc]] — HTTP Alternative Services; Alt-Svc header signals HTTP/3 availability --- diff --git a/notes/RFC/RFC9114/sections/00_preamble.md b/notes/RFC/RFC9114/sections/00_preamble.md index 71d40b5c6..1df068e81 100644 --- a/notes/RFC/RFC9114/sections/00_preamble.md +++ b/notes/RFC/RFC9114/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9114 rfc_section: "preamble" @@ -9,16 +9,11 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP ## Preamble - - - - Internet Engineering Task Force (IETF) M. Bishop, Ed. Request for Comments: 9114 Akamai Category: Standards Track June 2022 ISSN: 2070-1721 - HTTP/3 Abstract @@ -152,4 +147,3 @@ Table of Contents --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/02_1_introduction.md b/notes/RFC/RFC9114/sections/02_1_introduction.md index 481c8fdf2..8e3f64ad6 100644 --- a/notes/RFC/RFC9114/sections/02_1_introduction.md +++ b/notes/RFC/RFC9114/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9114 rfc_section: "1" @@ -63,4 +63,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/03_2_http3_protocol_overview.md b/notes/RFC/RFC9114/sections/03_2_http3_protocol_overview.md index b4a8f1537..582a3502f 100644 --- a/notes/RFC/RFC9114/sections/03_2_http3_protocol_overview.md +++ b/notes/RFC/RFC9114/sections/03_2_http3_protocol_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "2. HTTP/3 Protocol Overview" rfc_number: 9114 rfc_section: "2" @@ -151,4 +151,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/04_3_connection_setup_and_management.md b/notes/RFC/RFC9114/sections/04_3_connection_setup_and_management.md index b1d9456e2..30e4d8efd 100644 --- a/notes/RFC/RFC9114/sections/04_3_connection_setup_and_management.md +++ b/notes/RFC/RFC9114/sections/04_3_connection_setup_and_management.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Connection Setup and Management" rfc_number: 9114 rfc_section: "3" @@ -155,32 +155,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP particular origin can indicate that it is not authoritative for a request by sending a 421 (Misdirected Request) status code in response to the request; see Section 7.4 of [HTTP]. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes - -- **`Http3ControlStream.cs`** — Manages the HTTP/3 control stream lifecycle with state machine (`AwaitingSettings` → `Active` → `GoAway` → `Closed`); sends SETTINGS as first frame per §3.2 -- **`Http3Settings.cs`** — Encodes/decodes SETTINGS parameters using QUIC variable-length integers; supports `SETTINGS_MAX_FIELD_SECTION_SIZE` and reserved identifiers per §7.2.4.1 -- **`Http3Connection.cs`** — Connection lifecycle management including GOAWAY frame exchange for graceful shutdown per §3.3 -- **`QuicTransportAdapter.cs`** — QUIC transport abstraction bridging System.Net.Quic to TurboHTTP's connection model - -### Test References - -- `TurboHTTP.StreamTests/` — ~134 stream-level tests covering control stream state transitions and connection setup -- `TurboHTTP.Tests/RFC9114/` — 32 unit test files covering frame encoding, settings validation, error codes - -### Known Gaps - -- ❌ Alt-Svc discovery (§3.1.1) not implemented — connections use direct QUIC endpoints only -- ❌ Connection reuse certificate validation (§3.3) not implemented — each origin gets a dedicated connection -- ❌ 0-RTT QUIC resumption with stored SETTINGS (§7.2.4.2) not supported -- ⚠️ Server push streams (§6.2.2) not implemented — client-only library does not need to send push, but should reject server-initiated push gracefully - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/05_4_1_http_message_framing.md b/notes/RFC/RFC9114/sections/05_4_1_http_message_framing.md index 30d4dd4ae..ff9d97dcc 100644 --- a/notes/RFC/RFC9114/sections/05_4_1_http_message_framing.md +++ b/notes/RFC/RFC9114/sections/05_4_1_http_message_framing.md @@ -1,4 +1,4 @@ ---- +--- title: "4.1. HTTP Message Framing" rfc_number: 9114 rfc_section: "4.1" @@ -191,32 +191,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP intended to protect against several types of common attacks against HTTP; they are deliberately strict because being permissive can expose implementations to these vulnerabilities. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`Http3FrameDecoder.cs`** — Decodes HTTP/3 frame sequences (HEADERS → DATA* → HEADERS?) enforcing the valid message sequence per §4.1; raises `H3_FRAME_UNEXPECTED` for invalid frame ordering -- **`Http3FrameEncoder.cs`** — Encodes request messages as HEADERS + DATA frames with proper stream closure -- **`Http3RequestStream.cs`** — Manages bidirectional request stream lifecycle: sends request, closes send side, reads response per §4.1 requirements -- **`Http3ResponseDecoder.cs`** — Validates response frame sequences including interim (1xx) responses followed by final response; rejects `Transfer-Encoding` header per §4.1 - -### Test References - -- `TurboHTTP.Tests/RFC9114/01_Http3FrameDecoderTests.cs` — Frame sequence validation tests -- `TurboHTTP.Tests/RFC9114/02_Http3FrameEncoderTests.cs` — Frame encoding tests -- `TurboHTTP.Tests/RFC9114/03_Http3MessageFramingTests.cs` — Malformed message detection, Content-Length mismatch tests -- `TurboHTTP.StreamTests/` — Stream-level integration tests for full request/response exchanges - -### Known Gaps - -- ❌ PUSH_PROMISE interleaving (§4.1) — server push not implemented, PUSH_PROMISE frames rejected but not fully parsed -- ⚠️ Partial: `H3_REQUEST_INCOMPLETE` error sent when client stream terminates early, but edge cases around partial Content-Length remain under test - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/06_4_2_http_fields.md b/notes/RFC/RFC9114/sections/06_4_2_http_fields.md index 0954d493d..aba4354b9 100644 --- a/notes/RFC/RFC9114/sections/06_4_2_http_fields.md +++ b/notes/RFC/RFC9114/sections/06_4_2_http_fields.md @@ -1,4 +1,4 @@ ---- +--- title: "4.2. HTTP Fields" rfc_number: 9114 rfc_section: "4.2" @@ -81,31 +81,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP [HTTP]. Because this limit is applied separately by each implementation that processes the message, messages below this limit are not guaranteed to be accepted. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`QpackEncoder.cs`** — QPACK field compression with static and dynamic table support; lowercases field names per §4.2; splits Cookie headers per §4.2.1 -- **`QpackDecoder.cs`** — QPACK decompression with Cookie concatenation using `"; "` delimiter per §4.2.1; validates field name characters -- **`Http3HeaderValidator.cs`** — Rejects connection-specific headers (Connection, Keep-Alive, Transfer-Encoding, Upgrade) per §4.2; allows `TE: trailers` as sole exception -- **`Http3Settings.cs`** — Supports `SETTINGS_MAX_FIELD_SECTION_SIZE` (0x06) for header size constraint advertisement per §4.2.2 - -### Test References - -- `TurboHTTP.Tests/RFC9114/12_Http3QpackTests.cs` — QPACK encoding/decoding round-trips, static table lookups -- `TurboHTTP.Tests/RFC9114/13_Http3HeaderValidationTests.cs` — Connection-specific header rejection, uppercase field name detection, TE header validation -- `TurboHTTP.Tests/RFC9114/14_Http3CookieTests.cs` — Cookie splitting and concatenation per §4.2.1 - -### Known Gaps - -- ⚠️ QPACK dynamic table size is limited — encoder uses conservative settings to minimize head-of-line blocking at cost of compression ratio -- ⚠️ `SETTINGS_MAX_FIELD_SECTION_SIZE` is advertised but enforcement on received headers is approximate (checks uncompressed size estimate) - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/07_4_3_http_control_data.md b/notes/RFC/RFC9114/sections/07_4_3_http_control_data.md index 61aad8be1..bb8be4e07 100644 --- a/notes/RFC/RFC9114/sections/07_4_3_http_control_data.md +++ b/notes/RFC/RFC9114/sections/07_4_3_http_control_data.md @@ -1,4 +1,4 @@ ---- +--- title: "4.3. HTTP Control Data" rfc_number: 9114 rfc_section: "4.3" @@ -110,4 +110,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/08_4_4_the_connect_method.md b/notes/RFC/RFC9114/sections/08_4_4_the_connect_method.md index 7690c11de..09d9430b5 100644 --- a/notes/RFC/RFC9114/sections/08_4_4_the_connect_method.md +++ b/notes/RFC/RFC9114/sections/08_4_4_the_connect_method.md @@ -1,4 +1,4 @@ ---- +--- title: "4.4. The CONNECT Method" rfc_number: 9114 rfc_section: "4.4" @@ -86,4 +86,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/09_4_5_http_upgrade.md b/notes/RFC/RFC9114/sections/09_4_5_http_upgrade.md index 4a38e3cbc..580c87feb 100644 --- a/notes/RFC/RFC9114/sections/09_4_5_http_upgrade.md +++ b/notes/RFC/RFC9114/sections/09_4_5_http_upgrade.md @@ -1,4 +1,4 @@ ---- +--- title: "4.5. HTTP Upgrade" rfc_number: 9114 rfc_section: "4.5" @@ -17,4 +17,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/10_4_6_server_push.md b/notes/RFC/RFC9114/sections/10_4_6_server_push.md index d1fc88156..2e1884103 100644 --- a/notes/RFC/RFC9114/sections/10_4_6_server_push.md +++ b/notes/RFC/RFC9114/sections/10_4_6_server_push.md @@ -1,4 +1,4 @@ ---- +--- title: "4.6. Server Push" rfc_number: 9114 rfc_section: "4.6" @@ -118,4 +118,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/11_5_connection_closure.md b/notes/RFC/RFC9114/sections/11_5_connection_closure.md index c781e6cd9..f1da1b249 100644 --- a/notes/RFC/RFC9114/sections/11_5_connection_closure.md +++ b/notes/RFC/RFC9114/sections/11_5_connection_closure.md @@ -1,4 +1,4 @@ ---- +--- title: "5. Connection Closure" rfc_number: 9114 rfc_section: "5" @@ -162,32 +162,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP > **MUST**: If a connection terminates without a GOAWAY frame, clients MUST assume that any request that was sent, whether in whole or in part, might have been processed. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes - -- **`Http3Connection.cs`** — Implements graceful shutdown via GOAWAY frame exchange per §5.2; tracks last accepted stream ID; supports multiple GOAWAY frames with decreasing IDs -- **`Http3ControlStream.cs`** — Sends GOAWAY on control stream before connection closure per §5.2; uses `H3_NO_ERROR` for graceful close per §5.2 -- **`Http3IdleTimeoutHandler.cs`** — Monitors QUIC idle timeout and triggers reconnection per §5.1 -- **`QuicTransportAdapter.cs`** — Maps QUIC CONNECTION_CLOSE to TurboHTTP connection termination per §5.3 - -### Test References - -- `TurboHTTP.Tests/RFC9114/15_Http3ConnectionClosureTests.cs` — GOAWAY frame exchange, graceful shutdown sequence -- `TurboHTTP.Tests/RFC9114/16_Http3IdleTimeoutTests.cs` — Idle connection management -- `TurboHTTP.StreamTests/` — End-to-end connection lifecycle tests - -### Known Gaps - -- ❌ Two-phase GOAWAY shutdown (§5.2) — does not send initial max-value GOAWAY followed by final GOAWAY; sends single GOAWAY with actual last stream ID -- ⚠️ Client-to-server GOAWAY with push ID (§5.2) — not sent since server push is not implemented -- ⚠️ Transport closure (§5.4) — assumes unfinished requests failed on transport termination, but retry logic does not always distinguish processed vs. unprocessed requests - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/12_6_stream_mapping_and_usage.md b/notes/RFC/RFC9114/sections/12_6_stream_mapping_and_usage.md index 4867f6f66..dd3e62517 100644 --- a/notes/RFC/RFC9114/sections/12_6_stream_mapping_and_usage.md +++ b/notes/RFC/RFC9114/sections/12_6_stream_mapping_and_usage.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Stream Mapping and Usage" rfc_number: 9114 rfc_section: "6" @@ -198,35 +198,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP The payload and length of the stream are selected in any manner the sending implementation chooses. - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes - -- **`Http3RequestStream.cs`** — Uses client-initiated bidirectional QUIC streams for request/response per §6.1; stream IDs follow QUIC numbering (0, 4, 8, …) -- **`Http3ControlStream.cs`** — Creates a single unidirectional control stream (type 0x00) at connection start per §6.2.1; sends SETTINGS as first frame; rejects duplicate control streams with `H3_STREAM_CREATION_ERROR` -- **`Http3StreamTypeDecoder.cs`** — Reads stream type from unidirectional stream headers; routes to appropriate handler or aborts unknown types with `H3_STREAM_CREATION_ERROR` per §6.2 -- **`QpackEncoderStream.cs` / `QpackDecoderStream.cs`** — QPACK encoder and decoder unidirectional streams per §6.2 requirements - -### Test References - -- `TurboHTTP.Tests/RFC9114/04_Http3StreamTypeTests.cs` — Stream type identification and routing -- `TurboHTTP.Tests/RFC9114/05_Http3ControlStreamTests.cs` — Control stream lifecycle, SETTINGS-first validation -- `TurboHTTP.StreamTests/` — Stream multiplexing and bidirectional stream tests - -### Known Gaps - -- ❌ Push streams (§6.2.2) — not implemented; server-initiated push stream type (0x01) is rejected but push ID parsing is not validated -- ❌ Reserved stream types (§6.2.3) — not sent for connection padding; received reserved streams are correctly ignored -- ⚠️ Server-initiated bidirectional streams (§6.1) rejected with `H3_STREAM_CREATION_ERROR` as required, but error message could be more descriptive When sending a reserved stream type, -> **MAY**: the implementation MAY either terminate the stream cleanly or reset - it. When resetting the stream, either the H3_NO_ERROR error code or -> **SHOULD**: a reserved error code (Section 8.1) SHOULD be used. - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/13_7_1_frame_layout.md b/notes/RFC/RFC9114/sections/13_7_1_frame_layout.md index 921ff755b..5b13e3950 100644 --- a/notes/RFC/RFC9114/sections/13_7_1_frame_layout.md +++ b/notes/RFC/RFC9114/sections/13_7_1_frame_layout.md @@ -1,4 +1,4 @@ ---- +--- title: "7.1. Frame Layout" rfc_number: 9114 rfc_section: "7.1" @@ -89,29 +89,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP > **MUST**: truncated, this MUST be treated as a connection error of type H3_FRAME_ERROR. Streams that terminate abruptly may be reset at any point in a frame. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`Http3FrameDecoder.cs`** — Parses the `Type (i) + Length (i) + Payload (..)` format using QUIC variable-length integer decoding; validates payload length matches declared length; raises `H3_FRAME_ERROR` for truncated frames or length mismatches per §7.1 -- **`Http3FrameEncoder.cs`** — Encodes frames with variable-length integer Type and Length fields; all 7 defined frame types (DATA, HEADERS, CANCEL_PUSH, SETTINGS, PUSH_PROMISE, GOAWAY, MAX_PUSH_ID) use correct type codes -- **`QuicVariableLengthInteger.cs`** — Implements RFC 9000 §16 variable-length integer encoding/decoding used for frame Type and Length fields; validates self-consistency of redundant length encodings per §10.8 - -### Test References - -- `TurboHTTP.Tests/RFC9114/01_Http3FrameDecoderTests.cs` — Frame layout parsing, truncated frame detection, variable-length integer edge cases -- `TurboHTTP.Tests/RFC9114/02_Http3FrameEncoderTests.cs` — Round-trip encoding/decoding for all frame types -- `TurboHTTP.Tests/RFC9114/06_Http3FrameErrorTests.cs` — `H3_FRAME_ERROR` connection error tests for malformed frames - -### Known Gaps - -- None — frame layout parsing and validation is fully compliant with §7.1 - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/14_7_2_frame_definitions.md b/notes/RFC/RFC9114/sections/14_7_2_frame_definitions.md index 7c1bdf3f3..fbd6beb23 100644 --- a/notes/RFC/RFC9114/sections/14_7_2_frame_definitions.md +++ b/notes/RFC/RFC9114/sections/14_7_2_frame_definitions.md @@ -1,4 +1,4 @@ ---- +--- title: "7.2. Frame Definitions" rfc_number: 9114 rfc_section: "7.2" @@ -387,37 +387,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP Frame types that were used in HTTP/2 where there is no corresponding HTTP/3 frame have also been reserved (Section 11.2.1) - ---- - -## TurboHTTP Compliance - -**Status**: ⚠️ Partial - -### Implementation Notes - -- **`Http3FrameDecoder.cs`** — Decodes all 7 defined frame types: DATA (0x00), HEADERS (0x01), CANCEL_PUSH (0x03), SETTINGS (0x04), PUSH_PROMISE (0x05), GOAWAY (0x07), MAX_PUSH_ID (0x0d) -- **`Http3FrameEncoder.cs`** — Encodes DATA, HEADERS, SETTINGS, and GOAWAY frames; validates stream-type restrictions -- **`Http3Settings.cs`** — Full SETTINGS frame: `SETTINGS_MAX_FIELD_SECTION_SIZE`, reserved ID handling, duplicate detection, HTTP/2 setting rejection per §7.2.4 -- **`Http3GoAwayHandler.cs`** — GOAWAY processing with decreasing stream/push ID validation per §7.2.6 -- **`Http3ErrorCodes.cs`** — All 16 HTTP/3 error codes (0x0100–0x0110) - -### Test References - -- `TurboHTTP.Tests/RFC9114/01_Http3FrameDecoderTests.cs` — Frame type dispatch and payload parsing -- `TurboHTTP.Tests/RFC9114/02_Http3FrameEncoderTests.cs` — Encoding round-trips -- `TurboHTTP.Tests/RFC9114/07_Http3SettingsTests.cs` — SETTINGS validation -- `TurboHTTP.Tests/RFC9114/08_Http3GoAwayTests.cs` — GOAWAY frame processing - -### Known Gaps - -- ❌ CANCEL_PUSH (§7.2.3) — decoded but not acted upon (server push not implemented) -- ❌ PUSH_PROMISE (§7.2.5) — rejected with `H3_FRAME_UNEXPECTED` but push ID validation minimal -- ❌ MAX_PUSH_ID (§7.2.7) — not sent by client; server receipt correctly rejected -- ⚠️ Reserved frame types (§7.2.8) — ignored on receipt but not sent for padding. These frame -> **MUST NOT**: types MUST NOT be sent, and their receipt MUST be treated as a - connection error of type H3_FRAME_UNEXPECTED. - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/15_8_error_handling.md b/notes/RFC/RFC9114/sections/15_8_error_handling.md index 936a3df42..3648d2940 100644 --- a/notes/RFC/RFC9114/sections/15_8_error_handling.md +++ b/notes/RFC/RFC9114/sections/15_8_error_handling.md @@ -1,4 +1,4 @@ ---- +--- title: "8. Error Handling" rfc_number: 9114 rfc_section: "8" @@ -109,30 +109,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP error codes be treated as equivalent to H3_NO_ERROR (Section 9). > **SHOULD**: Implementations SHOULD select an error code from this space with some probability when they would have sent H3_NO_ERROR. - ---- - -## TurboHTTP Compliance - -**Status**: ✅ Compliant - -### Implementation Notes - -- **`Http3ErrorCodes.cs`** — Defines all 16 error codes from §8.1 with correct hex values: `H3_NO_ERROR` (0x0100) through `H3_VERSION_FALLBACK` (0x0110) -- **`Http3FrameDecoder.cs`** — Maps protocol violations to appropriate error codes; treats unknown error codes as `H3_NO_ERROR` per §8 -- **`Http3ControlStream.cs`** — Raises `H3_MISSING_SETTINGS` when control stream first frame is not SETTINGS; raises `H3_CLOSED_CRITICAL_STREAM` when control stream is closed -- **`Http3Connection.cs`** — Distinguishes stream errors from connection errors; escalates stream errors to connection errors when appropriate per §8 - -### Test References - -- `TurboHTTP.Tests/RFC9114/09_Http3ErrorCodeTests.cs` — Error code value validation, unknown code handling -- `TurboHTTP.Tests/RFC9114/10_Http3ConnectionErrorTests.cs` — Connection-level error propagation tests -- `TurboHTTP.Tests/RFC9114/11_Http3StreamErrorTests.cs` — Stream-level error isolation tests - -### Known Gaps - -- ⚠️ Reserved error codes (0x1f*N+0x21) are not probabilistically sent in place of `H3_NO_ERROR` per §8.1 SHOULD — always sends exact error code - ---- - -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/16_9_extensions_to_http3.md b/notes/RFC/RFC9114/sections/16_9_extensions_to_http3.md index 0d4dfaf25..54f3ceea5 100644 --- a/notes/RFC/RFC9114/sections/16_9_extensions_to_http3.md +++ b/notes/RFC/RFC9114/sections/16_9_extensions_to_http3.md @@ -1,4 +1,4 @@ ---- +--- title: "9. Extensions to HTTP/3" rfc_number: 9114 rfc_section: "9" @@ -55,4 +55,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/17_10_security_considerations.md b/notes/RFC/RFC9114/sections/17_10_security_considerations.md index 8834a6632..ac3967596 100644 --- a/notes/RFC/RFC9114/sections/17_10_security_considerations.md +++ b/notes/RFC/RFC9114/sections/17_10_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "10. Security Considerations" rfc_number: 9114 rfc_section: "10" @@ -264,4 +264,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/18_11_1_registration_of_http3_identification_string.md b/notes/RFC/RFC9114/sections/18_11_1_registration_of_http3_identification_string.md index a49c38e10..ca9c880fb 100644 --- a/notes/RFC/RFC9114/sections/18_11_1_registration_of_http3_identification_string.md +++ b/notes/RFC/RFC9114/sections/18_11_1_registration_of_http3_identification_string.md @@ -1,4 +1,4 @@ ---- +--- title: "11.1. Registration of HTTP/3 Identification String" rfc_number: 9114 rfc_section: "11.1" @@ -31,4 +31,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/19_11_2_new_registries.md b/notes/RFC/RFC9114/sections/19_11_2_new_registries.md index 73d06edec..d6f30a2ec 100644 --- a/notes/RFC/RFC9114/sections/19_11_2_new_registries.md +++ b/notes/RFC/RFC9114/sections/19_11_2_new_registries.md @@ -1,4 +1,4 @@ ---- +--- title: "11.2. New Registries" rfc_number: 9114 rfc_section: "11.2" @@ -293,4 +293,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/86_12_references.md b/notes/RFC/RFC9114/sections/86_12_references.md index a590acd46..0984ea7e6 100644 --- a/notes/RFC/RFC9114/sections/86_12_references.md +++ b/notes/RFC/RFC9114/sections/86_12_references.md @@ -1,4 +1,4 @@ ---- +--- title: "12. References" rfc_number: 9114 rfc_section: "12" @@ -123,4 +123,3 @@ tags: [RFC9114, HTTP/3, QUIC, variable-length-frames, unidirectional-streams, QP --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9114/sections/91_appendix_a_considerations_for_transitioning_from_http2.md b/notes/RFC/RFC9114/sections/91_appendix_a_considerations_for_transitioning_from_http2.md index 1779924fb..2e722a551 100644 --- a/notes/RFC/RFC9114/sections/91_appendix_a_considerations_for_transitioning_from_http2.md +++ b/notes/RFC/RFC9114/sections/91_appendix_a_considerations_for_transitioning_from_http2.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Considerations for Transitioning from HTTP/2" rfc_number: 9114 rfc_section: "Appendix A" @@ -371,4 +371,3 @@ Acknowledgments --- -**Navigation:** [[../RFC9114|RFC9114 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/RFC9204.md b/notes/RFC/RFC9204/RFC9204.md index 5942c4098..505435f86 100644 --- a/notes/RFC/RFC9204/RFC9204.md +++ b/notes/RFC/RFC9204/RFC9204.md @@ -1,4 +1,4 @@ ---- +--- title: "RFC 9204 — QPACK: Field Compression for HTTP/3" rfc_number: 9204 description: "QPACK header compression for HTTP/3. Defines static/dynamic tables, encoder/decoder instruction streams, blocking references, and section acknowledgment." @@ -10,17 +10,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9204" **Official RFC**: [RFC 9204](https://www.rfc-editor.org/rfc/rfc9204) -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | 40/100 | -| **Implementation Status** | 🟡 Draft | -| **Implementation Path** | `TurboHTTP/Protocol/RFC9204/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFC9204/` — 11 files | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFC9204/` | -| **Key Gaps** | Encoder side, instruction processing, capacity management, section acknowledgment, stream cancellation | - ## Core Concepts - [[RFC9204/sections/03_2_compression_process_overview|§2 Compression Process Overview]] — how QPACK avoids head-of-line blocking @@ -31,29 +20,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9204" - [[RFC9204/sections/08_4_4_decoder_instructions|§4.4 Decoder Instructions]] — section acknowledgment, stream cancellation - [[RFC9204/sections/09_4_5_field_line_representations|§4.5 Field Line Representations]] — indexed, literal, post-base -## Implementation Notes - -### Decoder - -| Component | File | Purpose | -|-----------|------|---------| -| `QpackDecoder` | `Protocol/RFC9204/QpackDecoder.cs` | Header decompression with dynamic table | -| `QpackDecoderInstructionWriter` | `Protocol/RFC9204/QpackDecoderInstructionWriter.cs` | Decoder instruction generation | - -### Stages - -| Stage | File | Purpose | -|-------|------|---------| -| `QpackEncoderStreamStage` | `Streams/Stages/Encoding/QpackEncoderStreamStage.cs` | QPACK encoder instructions in pipeline | -| `QpackDecoderStreamStage` | `Streams/Stages/Decoding/QpackDecoderStreamStage.cs` | QPACK decoder instructions in pipeline | - -### Tests - -| Test File | Coverage | -|-----------|----------| -| `TurboHTTP.Tests/RFC9204/` | 11 test files — decoder, static table, instructions | -| `TurboHTTP.StreamTests/RFC9204/` | Stage behaviour tests — encoder/decoder stream stages | - ## Sections | # | Section | File | Status | @@ -89,7 +55,6 @@ source_url: "https://www.rfc-editor.org/rfc/rfc9204" - [[RFC7541/RFC7541|RFC 7541 — HPACK]] — HTTP/2 header compression (predecessor) - [[RFC9114/RFC9114|RFC 9114 — HTTP/3]] — protocol using QPACK - [[RFC9000/RFC9000|RFC 9000 — QUIC]] — underlying transport -- [[00-RFC_STATUS_MATRIX|RFC Compliance Matrix]] — overall compliance tracking --- diff --git a/notes/RFC/RFC9204/sections/00_preamble.md b/notes/RFC/RFC9204/sections/00_preamble.md index 93ec882fa..34c26c13c 100644 --- a/notes/RFC/RFC9204/sections/00_preamble.md +++ b/notes/RFC/RFC9204/sections/00_preamble.md @@ -1,4 +1,4 @@ ---- +--- title: "Preamble" rfc_number: 9204 rfc_section: "preamble" @@ -9,10 +9,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, ## Preamble - - - - Internet Engineering Task Force (IETF) C. Krasic Request for Comments: 9204 Category: Standards Track M. Bishop @@ -21,7 +17,6 @@ ISSN: 2070-1721 Akamai Technologies Facebook June 2022 - QPACK: Field Compression for HTTP/3 Abstract @@ -135,4 +130,3 @@ Table of Contents --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/02_1_introduction.md b/notes/RFC/RFC9204/sections/02_1_introduction.md index e2c8e03f7..d20b541ff 100644 --- a/notes/RFC/RFC9204/sections/02_1_introduction.md +++ b/notes/RFC/RFC9204/sections/02_1_introduction.md @@ -1,4 +1,4 @@ ---- +--- title: "1. Introduction" rfc_number: 9204 rfc_section: "1" @@ -88,4 +88,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/03_2_compression_process_overview.md b/notes/RFC/RFC9204/sections/03_2_compression_process_overview.md index e458cff16..9fabc168e 100644 --- a/notes/RFC/RFC9204/sections/03_2_compression_process_overview.md +++ b/notes/RFC/RFC9204/sections/03_2_compression_process_overview.md @@ -1,4 +1,4 @@ ---- +--- title: "2. Compression Process Overview" rfc_number: 9204 rfc_section: "2" @@ -281,4 +281,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/04_3_reference_tables.md b/notes/RFC/RFC9204/sections/04_3_reference_tables.md index 3c96fbbac..9fd18a4a7 100644 --- a/notes/RFC/RFC9204/sections/04_3_reference_tables.md +++ b/notes/RFC/RFC9204/sections/04_3_reference_tables.md @@ -1,4 +1,4 @@ ---- +--- title: "3. Reference Tables" rfc_number: 9204 rfc_section: "3" @@ -143,13 +143,11 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, | V Insertion Point Dropping Point - ```abnf n = count of entries inserted d = count of entries dropped ``` - Figure 2: Example Dynamic Table Indexing - Encoder Stream Unlike in encoder instructions, relative indices in field line @@ -170,7 +168,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, | 0 | ... | n-d-3 | Relative Index +-----+-----+-------+ - ```abnf n = count of entries inserted d = count of entries dropped @@ -201,7 +198,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, | 1 | 0 | Post-Base Index +-----+-----+ - ```abnf n = count of entries inserted d = count of entries dropped @@ -214,4 +210,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/05_4_1_primitives.md b/notes/RFC/RFC9204/sections/05_4_1_primitives.md index 049e9458d..1f2284bfd 100644 --- a/notes/RFC/RFC9204/sections/05_4_1_primitives.md +++ b/notes/RFC/RFC9204/sections/05_4_1_primitives.md @@ -1,4 +1,4 @@ ---- +--- title: "4.1. Primitives" rfc_number: 9204 rfc_section: "4.1" @@ -51,4 +51,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/06_4_2_encoder_and_decoder_streams.md b/notes/RFC/RFC9204/sections/06_4_2_encoder_and_decoder_streams.md index 6d7603ff6..2175c0ed2 100644 --- a/notes/RFC/RFC9204/sections/06_4_2_encoder_and_decoder_streams.md +++ b/notes/RFC/RFC9204/sections/06_4_2_encoder_and_decoder_streams.md @@ -1,4 +1,4 @@ ---- +--- title: "4.2. Encoder and Decoder Streams" rfc_number: 9204 rfc_section: "4.2" @@ -44,4 +44,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/07_4_3_encoder_instructions.md b/notes/RFC/RFC9204/sections/07_4_3_encoder_instructions.md index 2256756d2..ed100d564 100644 --- a/notes/RFC/RFC9204/sections/07_4_3_encoder_instructions.md +++ b/notes/RFC/RFC9204/sections/07_4_3_encoder_instructions.md @@ -1,4 +1,4 @@ ---- +--- title: "4.3. Encoder Instructions" rfc_number: 9204 rfc_section: "4.3" @@ -117,4 +117,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/08_4_4_decoder_instructions.md b/notes/RFC/RFC9204/sections/08_4_4_decoder_instructions.md index dbaad1105..8e45ad4a7 100644 --- a/notes/RFC/RFC9204/sections/08_4_4_decoder_instructions.md +++ b/notes/RFC/RFC9204/sections/08_4_4_decoder_instructions.md @@ -1,4 +1,4 @@ ---- +--- title: "4.4. Decoder Instructions" rfc_number: 9204 rfc_section: "4.4" @@ -79,4 +79,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/09_4_5_field_line_representations.md b/notes/RFC/RFC9204/sections/09_4_5_field_line_representations.md index 0d10d124a..d09a59fc1 100644 --- a/notes/RFC/RFC9204/sections/09_4_5_field_line_representations.md +++ b/notes/RFC/RFC9204/sections/09_4_5_field_line_representations.md @@ -1,4 +1,4 @@ ---- +--- title: "4.5. Field Line Representations" rfc_number: 9204 rfc_section: "4.5" @@ -57,17 +57,14 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, EncInsertCount = (ReqInsertCount mod (2 * MaxEntries)) + 1 ``` - Here MaxEntries is the maximum number of entries that the dynamic table can have. The smallest entry has empty name and value strings and has the size of 32. Hence, MaxEntries is calculated as: - ```abnf MaxEntries = floor( MaxTableCapacity / 32 ) ``` - MaxTableCapacity is the maximum capacity of the dynamic table as specified by the decoder; see Section 3.2.3. @@ -83,7 +80,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, TotalNumberOfInserts is the total number of inserts into the decoder's dynamic table. - ```abnf FullRange = 2 * MaxEntries if EncodedInsertCount == 0: @@ -94,7 +90,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, MaxValue = TotalNumberOfInserts + MaxEntries ``` - # MaxWrapped is the largest possible value of # ReqInsertCount that is 0 mod 2 * MaxEntries @@ -103,7 +98,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, ReqInsertCount = MaxWrapped + EncodedInsertCount - 1 ``` - # If ReqInsertCount exceeds MaxValue, the Encoder's value # must have wrapped one fewer time if ReqInsertCount > MaxValue: @@ -143,7 +137,6 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, Base = ReqInsertCount - DeltaBase - 1 ``` - A single-pass encoder determines the Base before encoding a field section. If the encoder inserted entries in the dynamic table while encoding the field section and is referencing them, Required Insert @@ -304,4 +297,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/10_5_configuration.md b/notes/RFC/RFC9204/sections/10_5_configuration.md index dec806738..e51f9e17a 100644 --- a/notes/RFC/RFC9204/sections/10_5_configuration.md +++ b/notes/RFC/RFC9204/sections/10_5_configuration.md @@ -1,4 +1,4 @@ ---- +--- title: "5. Configuration" rfc_number: 9204 rfc_section: "5" @@ -22,4 +22,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/11_6_error_handling.md b/notes/RFC/RFC9204/sections/11_6_error_handling.md index 51c2276d7..fd10a100d 100644 --- a/notes/RFC/RFC9204/sections/11_6_error_handling.md +++ b/notes/RFC/RFC9204/sections/11_6_error_handling.md @@ -1,4 +1,4 @@ ---- +--- title: "6. Error Handling" rfc_number: 9204 rfc_section: "6" @@ -26,4 +26,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/12_7_security_considerations.md b/notes/RFC/RFC9204/sections/12_7_security_considerations.md index 81f2e26f7..67115d0af 100644 --- a/notes/RFC/RFC9204/sections/12_7_security_considerations.md +++ b/notes/RFC/RFC9204/sections/12_7_security_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "7. Security Considerations" rfc_number: 9204 rfc_section: "7" @@ -266,4 +266,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/13_8_iana_considerations.md b/notes/RFC/RFC9204/sections/13_8_iana_considerations.md index fc3b8d150..a40a21a88 100644 --- a/notes/RFC/RFC9204/sections/13_8_iana_considerations.md +++ b/notes/RFC/RFC9204/sections/13_8_iana_considerations.md @@ -1,4 +1,4 @@ ---- +--- title: "8. IANA Considerations" rfc_number: 9204 rfc_section: "8" @@ -77,4 +77,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/86_9_references.md b/notes/RFC/RFC9204/sections/86_9_references.md index 62f9253fa..68f331e80 100644 --- a/notes/RFC/RFC9204/sections/86_9_references.md +++ b/notes/RFC/RFC9204/sections/86_9_references.md @@ -1,4 +1,4 @@ ---- +--- title: "9. References" rfc_number: 9204 rfc_section: "9" @@ -72,4 +72,3 @@ tags: [RFC9204, QPACK, header-compression, HTTP/3, dynamic-table, static-table, --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/91_appendix_a_static_table.md b/notes/RFC/RFC9204/sections/91_appendix_a_static_table.md index 9e7447c4d..e1290a56b 100644 --- a/notes/RFC/RFC9204/sections/91_appendix_a_static_table.md +++ b/notes/RFC/RFC9204/sections/91_appendix_a_static_table.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix A. Static Table" rfc_number: 9204 rfc_section: "Appendix A" @@ -240,4 +240,3 @@ Appendix A. Static Table --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/92_appendix_b_encoding_and_decoding_examples.md b/notes/RFC/RFC9204/sections/92_appendix_b_encoding_and_decoding_examples.md index 179d45c84..e1f436b70 100644 --- a/notes/RFC/RFC9204/sections/92_appendix_b_encoding_and_decoding_examples.md +++ b/notes/RFC/RFC9204/sections/92_appendix_b_encoding_and_decoding_examples.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix B. Encoding and Decoding Examples" rfc_number: 9204 rfc_section: "Appendix B" @@ -193,4 +193,3 @@ B.5. Dynamic Table Insert, Eviction --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/RFC/RFC9204/sections/93_appendix_c_sample_single-pass_encoding_algorithm.md b/notes/RFC/RFC9204/sections/93_appendix_c_sample_single-pass_encoding_algorithm.md index 278daa24d..09fa0fba4 100644 --- a/notes/RFC/RFC9204/sections/93_appendix_c_sample_single-pass_encoding_algorithm.md +++ b/notes/RFC/RFC9204/sections/93_appendix_c_sample_single-pass_encoding_algorithm.md @@ -1,4 +1,4 @@ ---- +--- title: "Appendix C. Sample Single-Pass Encoding Algorithm" rfc_number: 9204 rfc_section: "Appendix C" @@ -54,7 +54,6 @@ Appendix C. Sample Single-Pass Encoding Algorithm encodeStaticIndexReference(streamBuffer, staticIndex) continue - ```abnf dynamicIndex = dynamicTable.findIndex(line) ``` @@ -68,7 +67,6 @@ Appendix C. Sample Single-Pass Encoding Algorithm dynamicNameIndex = dynamicTable.findName(line.name) ``` - if shouldIndex(line) and dynamicTable.canIndex(line): encodeInsert(encoderBuffer, staticNameIndex, dynamicNameIndex, line) @@ -77,7 +75,6 @@ Appendix C. Sample Single-Pass Encoding Algorithm dynamicIndex = dynamicTable.add(line) ``` - if dynamicIndex is None: # Could not index it, literal if dynamicNameIndex is not None: @@ -103,7 +100,6 @@ Appendix C. Sample Single-Pass Encoding Algorithm encodeDynamicIndexReference(streamBuffer, dynamicIndex, base) ``` - # encode the prefix if requiredInsertCount == 0: encodeInteger(prefixBuffer, 0x00, 0, 8) @@ -161,4 +157,3 @@ Acknowledgments --- -**Navigation:** [[../RFC9204|RFC9204 Index]] | [[../../00-RFC_STATUS_MATRIX|Status Matrix]] diff --git a/notes/Refactoring/Wave-2-Spec-Cleanup-Results b/notes/Refactoring/Wave-2-Spec-Cleanup-Results deleted file mode 100644 index 0999c1efc..000000000 --- a/notes/Refactoring/Wave-2-Spec-Cleanup-Results +++ /dev/null @@ -1,64 +0,0 @@ -# Wave 2 Spec Cleanup Results - -## Task Completion -Successfully applied Wave 2 refactoring to remove single-line depth-1 `//` comments, RFC traits, and XML documentation from test specifications across two directories. - -## Target Directories -- `src/TurboHTTP.StreamTests/Streams/` (17 spec files modified) -- `src/TurboHTTP.StreamTests/Transport/` (12 spec files modified) - -## Changes Applied - -### Files Modified: 29 spec files (primary targets) -- **Streams Directory**: 17 files - - ConnectionStageSpec.cs - - EngineBidiFlowCompositionSpec.cs - - EnginePipelineDescriptorSpec.cs - - FeedbackBufferOptimizationSpec.cs - - GroupByEndpointFanOutSpec.cs - - GroupByHostKeyQueueSizeSpec.cs - - HandlerBidiStageSpec.cs - - HostKeySubFlowSpec.cs - - Internal/NetworkBufferBatchStageSpec.cs - - Lifecycle/ClientStreamOwnerSpec.cs - - LoopbackBenchmarkStageSpec.cs - - RefererSanitizationSpec.cs - - StageCompletionRegressionSpec.cs - - StageOrderingIntegrationSpec.cs - - StageOrderingSpec.cs - - TransportRegistrySpec.cs - - VersionDispatchCachingSpec.cs - -- **Transport Directory**: 12 files - - ConnectionManagerActorSpec.cs - - QuicConnectionManagerActorSpec.cs - - QuicConnectionStageSpec.cs - - QuicPumpManagerSpec.cs - - QuicStreamRouterEnhancedSpec.cs - - QuicStreamRouterSpec.cs - - QuicTransportStateMachineLifecycleSpec.cs - - QuicTransportStateMachineSpec.cs - - TcpTransportStateMachineDataFlowSpec.cs - - TcpTransportStateMachineErrorSpec.cs - - TcpTransportStateMachineLifecycleSpec.cs - - TcpTransportStateMachineSpec.cs - -### Cleanup Operations Performed -1. **Depth-1 Comments Removed**: Single-line `//` comments at class body level -2. **RFC Traits Removed**: `[Trait("RFC", ...)]` attributes from non-Protocol folders -3. **XML Doc Comments Removed**: `///` documentation outside method bodies -4. **Blank Line Consolidation**: Consecutive blank lines collapsed to single lines - -### Total Changes -- **Total files changed**: 228 (includes broader refactoring across TurboHTTP.Tests) -- **Lines deleted**: 1,988 -- **Lines inserted**: 146 -- **Primary scope (Streams + Transport)**: 29 spec files, ~138 lines removed - -## Verification -All changes are staged and ready for commit. No compilation errors expected as only comments and decorative attributes were removed. - -## Session Context -- Continuation of previous conversation that ran out of context -- Background agents completed Wave 2 refactoring tasks for multiple test directories -- Changes applied via parallel Edit operations maintaining brace-depth tracking diff --git a/notes/Templates/ADR.md b/notes/Templates/ADR.md deleted file mode 100644 index d0faaf56b..000000000 --- a/notes/Templates/ADR.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -date: {{date}} -status: proposed | accepted | superseded | deprecated ---- - -# ADR: {{title}} - -## Status -Proposed - -## Context - - -## Decision - - -## Consequences - -### Positive -- - -### Negative -- - -## Alternatives Considered -- diff --git a/notes/Templates/Bug-Investigation.md b/notes/Templates/Bug-Investigation.md deleted file mode 100644 index f2e3730cb..000000000 --- a/notes/Templates/Bug-Investigation.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -date: {{date}} -feature: -severity: low | medium | high | critical -status: open | in-progress | resolved ---- - -# Bug: {{title}} - -## Symptom - - -## Reproduction Steps -1. - -## Hypothesis - - -## Trace / Diagnostic Output - - -## Root Cause - - -## Fix Applied - - -## References -- [Related feature] — Link to feature note if applicable -- [Related debugging notes] — Link to other investigation notes diff --git a/notes/Templates/RFC-Index.md b/notes/Templates/RFC-Index.md deleted file mode 100644 index c9ec13f94..000000000 --- a/notes/Templates/RFC-Index.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: "RFC XXXX — Protocol Name" -rfc_number: XXXX -source_url: https://www.rfc-editor.org/rfc/rfcXXXX -description: "One-line description of the RFC scope and TurboHTTP relevance" -tags: [rfc, rfcXXXX, protocol-category] ---- - -# RFC XXXX — Protocol Name - -> 📌 **External Source**: [RFC XXXX — Protocol Name](https://www.rfc-editor.org/rfc/rfcXXXX) -> -> The complete RFC text is available online. See the `sections/` subfolder for individual section references. - -## Quick Reference - -| Metric | Value | -|--------|-------| -| **Compliance Score** | XX/100 | -| **Implementation Status** | ✅ Complete / 🔶 Partial / 🟡 Draft / ❌ Missing | -| **Implementation Path** | `TurboHTTP/Protocol/RFCXXXX/` | -| **Unit Test Files** | `TurboHTTP.Tests/RFCXXXX/` — N files, M tests | -| **Stream Test Files** | `TurboHTTP.StreamTests/RFCXXXX/` — N files | -| **Key Gaps** | Brief summary of main gaps | - -## Core Concepts - -Key ideas from this RFC, with links to section files: - -- [[RFCXXXX/sections/NN_topic|Topic Name]] — brief description -- [[RFCXXXX/sections/NN_topic|Topic Name]] — brief description - -## Implementation Notes - -### Encoder - -| File | Purpose | -|------|---------| -| `Protocol/RFCXXXX/EncoderFile.cs` | Description | - -### Decoder - -| File | Purpose | -|------|---------| -| `Protocol/RFCXXXX/DecoderFile.cs` | Description | - -### Stages - -| File | Purpose | -|------|---------| -| `Streams/Stages/Encoding/StageFile.cs` | Description | -| `Streams/Stages/Decoding/StageFile.cs` | Description | - -### Tests - -| Location | Count | Focus | -|----------|-------|-------| -| `TurboHTTP.Tests/RFCXXXX/` | N tests | Protocol compliance | -| `TurboHTTP.StreamTests/RFCXXXX/` | N tests | Stage behaviour | - -## Sections - -| # | Section | File | Status | -|---|---------|------|--------| -| 00 | Preamble | [[RFCXXXX/sections/00_preamble\|00 Preamble]] | ✅ | -| 01 | Section Title | [[RFCXXXX/sections/NN_name\|Section Title]] | ✅ / 🔶 / 🟡 | - -## Dependencies - -| Direction | RFC | Relationship | -|-----------|-----|--------------| -| **Depends on** | [[../RFCXXXX/RFCXXXX\|RFC XXXX]] | Description | -| **Used by** | [[../RFCXXXX/RFCXXXX\|RFC XXXX]] | Description | - -## See Also - -- [[../00-RFC_STATUS_MATRIX|RFC Status Matrix]] -- [[../../Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|Known Gaps]] diff --git a/notes/Templates/RFC-Note.md b/notes/Templates/RFC-Note.md deleted file mode 100644 index 5c5123384..000000000 --- a/notes/Templates/RFC-Note.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: RFC Compliance Gap Template -description: >- - Template for documenting RFC compliance gaps and limitations (distinct from - RFC-Index.md) -tags: - - template - - rfc - - gaps -aliases: - - RFC Gap Template - - Compliance Gap ---- - -# RFC {{rfc_number}}: {{gap_title}} - -## Overview - -Brief description of the compliance gap or limitation. This note documents **specific gaps within an RFC**, not the RFC overview (that goes in `RFC-Index.md`). - -## Affected Section(s) - -- RFC {{rfc_number}} Section X: {{section_name}} — [[../RFC{{rfc_number}}/{{rfc_number}}.md|See RFC Index]] - -## Gap Description - -### Current Behavior -What TurboHTTP currently does (or doesn't do). - -### RFC Requirement -What the RFC specifies or requires. - -### Impact -- **On compliance**: Affects RFC {{rfc_number}} compliance score by ±X% -- **On users**: How this limitation affects users (if at all) -- **On performance**: Performance implications, if any - -## Workaround - -If a workaround exists, document it: -- Workaround approach -- Limitations of workaround - -## Test Coverage - -- Unit tests: {{X}} tests in `TurboHTTP.Tests/RFC{{rfc_number}}/` -- Integration tests: {{Y}} tests in `TurboHTTP.IntegrationTests/` -- Gap coverage: ✅ / 🔶 / ❌ - -## Priority - -- **Critical** (blocks production) -- **High** (affects many users) -- **Medium** (affects some users) -- **Low** (edge case) - -## Related Notes - -- [[../RFC/00-RFC_STATUS_MATRIX|RFC Status Matrix]] — Overall compliance tracking -- [[../Architecture/Status/03-KNOWN_GAPS_AND_LIMITATIONS|All Known Gaps]] — Cross-RFC gap summary -- {{link to related RFC gap notes}} - -## References - -- [RFC {{rfc_number}} Section X](https://www.rfc-editor.org/rfc/rfc{{rfc_number}}#section-x) — RFC text -- [[../../Features/feature_name|Feature Plan]] — Related feature (if applicable) -- `{{file_path}}:{{line_number}}` — Code location diff --git a/notes/Templates/Session-Log.md b/notes/Templates/Session-Log.md deleted file mode 100644 index 53a080688..000000000 --- a/notes/Templates/Session-Log.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Session Log Template -description: Daily work capture template for session tracking -tags: - - template - - meta - - sessions -aliases: - - Session Template - - Daily Log ---- - -# Session Log: {{date}} - -**Branch**: {{branch}} -**RUN_ID**: {{run_id}} - -## Work Completed - -### Task(s) -- TASK-XXX-XXX: Task title - -### Changes Made -- File changes summary -- Key implementations - -## Discoveries - -### Non-obvious learnings -- Architecture insight -- Platform quirk -- Performance finding - -### Links to Documentation -- Related Obsidian notes -- Architecture decisions -- Test files - -## Open Questions - -- [ ] Question 1 — blocking / non-blocking -- [ ] Question 2 — status - -## References - -- [[../00-Index|Vault Index]] -- [[../Architecture/Status/04-CURRENT_STATE_SUMMARY|Project Status]] -- Related feature or RFC notes diff --git a/src/Servus.Akka.Tests/Diagnostics/LoggerServusTraceListenerSpec.cs b/src/Servus.Akka.Tests/Diagnostics/LoggerServusTraceListenerSpec.cs new file mode 100644 index 000000000..695166322 --- /dev/null +++ b/src/Servus.Akka.Tests/Diagnostics/LoggerServusTraceListenerSpec.cs @@ -0,0 +1,165 @@ +using Microsoft.Extensions.Logging; +using Servus.Akka.Diagnostics; + +namespace Servus.Akka.Tests.Diagnostics; + +[CollectionDefinition("OTEL", DisableParallelization = true)] +public sealed class OTelCollection; + +[Collection("OTEL")] +public sealed class LoggerServusTraceListenerSpec : IDisposable +{ + private sealed class CapturingLogger : ILogger + { + public List<(LogLevel Level, string Message)> Entries { get; } = []; + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + Entries.Add((logLevel, formatter(state, exception))); + } + } + + private sealed class CapturingLoggerFactory : ILoggerFactory + { + private readonly Dictionary _loggers = new(); + + public CapturingLogger GetLogger(string name) + { + if (!_loggers.TryGetValue(name, out var logger)) + { + logger = new CapturingLogger(); + _loggers[name] = logger; + } + + return logger; + } + + public ILogger CreateLogger(string categoryName) => GetLogger(categoryName); + + public void AddProvider(ILoggerProvider provider) + { + } + + public void Dispose() + { + } + } + + private readonly CapturingLoggerFactory _factory = new(); + + public void Dispose() + { + ServusTrace.Disable(); + } + + [Fact(Timeout = 5000)] + public void IsEnabled_should_return_false_when_level_below_minimum() + { + var listener = new LoggerServusTraceListener(_factory, ServusTraceCategory.All, ServusTraceLevel.Warning); + + Assert.False(listener.IsEnabled(ServusTraceLevel.Debug, ServusTraceCategory.Connection)); + Assert.False(listener.IsEnabled(ServusTraceLevel.Info, ServusTraceCategory.Pool)); + } + + [Fact(Timeout = 5000)] + public void IsEnabled_should_return_false_when_category_not_enabled() + { + var listener = new LoggerServusTraceListener(_factory, ServusTraceCategory.Connection, ServusTraceLevel.Trace); + + Assert.False(listener.IsEnabled(ServusTraceLevel.Debug, ServusTraceCategory.Dns)); + Assert.False(listener.IsEnabled(ServusTraceLevel.Error, ServusTraceCategory.Pool)); + } + + [Fact(Timeout = 5000)] + public void IsEnabled_should_return_true_when_level_and_category_match() + { + var listener = new LoggerServusTraceListener(_factory); + + Assert.True(listener.IsEnabled(ServusTraceLevel.Debug, ServusTraceCategory.Connection)); + Assert.True(listener.IsEnabled(ServusTraceLevel.Error, ServusTraceCategory.Tls)); + } + + [Fact(Timeout = 5000)] + public void Write_should_route_Connection_event_to_correct_logger() + { + var listener = new LoggerServusTraceListener(_factory); + var source = new object(); + var evt = new ServusTraceEvent( + System.Diagnostics.Stopwatch.GetTimestamp(), + ServusTraceLevel.Debug, + ServusTraceCategory.Connection, + source.GetType().Name, source.GetHashCode(), "Connected to {0}:{1}", "localhost", 443); + + listener.Write(in evt); + + var logger = _factory.GetLogger("Servus.Akka.Trace.Connection"); + Assert.Single(logger.Entries); + Assert.Equal(LogLevel.Debug, logger.Entries[0].Level); + Assert.Contains("Connected to localhost:443", logger.Entries[0].Message); + } + + [Fact(Timeout = 5000)] + public void Write_should_route_Dns_event_to_Dns_logger() + { + var listener = new LoggerServusTraceListener(_factory); + var source = new object(); + var evt = new ServusTraceEvent( + System.Diagnostics.Stopwatch.GetTimestamp(), + ServusTraceLevel.Warning, + ServusTraceCategory.Dns, + source.GetType().Name, source.GetHashCode(), "DNS failed"); + + listener.Write(in evt); + + var logger = _factory.GetLogger("Servus.Akka.Trace.Dns"); + Assert.Single(logger.Entries); + Assert.Equal(LogLevel.Warning, logger.Entries[0].Level); + } + + [Fact(Timeout = 5000)] + public void Write_should_route_Pool_event_to_Pool_logger() + { + var listener = new LoggerServusTraceListener(_factory); + var source = new object(); + var evt = new ServusTraceEvent( + System.Diagnostics.Stopwatch.GetTimestamp(), + ServusTraceLevel.Info, + ServusTraceCategory.Pool, + source.GetType().Name, source.GetHashCode(), "Pool evicted"); + + listener.Write(in evt); + + var poolLogger = _factory.GetLogger("Servus.Akka.Trace.Pool"); + Assert.Single(poolLogger.Entries); + } + + [Fact(Timeout = 5000)] + public void Write_should_map_ServusTraceLevel_to_LogLevel_correctly() + { + var listener = new LoggerServusTraceListener(_factory, ServusTraceCategory.All, ServusTraceLevel.Trace); + var source = new object(); + + void Send(ServusTraceLevel level) => + listener.Write(new ServusTraceEvent( + System.Diagnostics.Stopwatch.GetTimestamp(), level, ServusTraceCategory.Tls, + source.GetType().Name, source.GetHashCode(), "msg")); + + Send(ServusTraceLevel.Trace); + Send(ServusTraceLevel.Debug); + Send(ServusTraceLevel.Info); + Send(ServusTraceLevel.Warning); + Send(ServusTraceLevel.Error); + + var logger = _factory.GetLogger("Servus.Akka.Trace.Tls"); + Assert.Equal(5, logger.Entries.Count); + Assert.Equal(LogLevel.Trace, logger.Entries[0].Level); + Assert.Equal(LogLevel.Debug, logger.Entries[1].Level); + Assert.Equal(LogLevel.Information, logger.Entries[2].Level); + Assert.Equal(LogLevel.Warning, logger.Entries[3].Level); + Assert.Equal(LogLevel.Error, logger.Entries[4].Level); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.Tests/Diagnostics/ServusTraceExtensionsSpec.cs b/src/Servus.Akka.Tests/Diagnostics/ServusTraceExtensionsSpec.cs new file mode 100644 index 000000000..40c81b6f4 --- /dev/null +++ b/src/Servus.Akka.Tests/Diagnostics/ServusTraceExtensionsSpec.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.DependencyInjection; +using Servus.Akka.Diagnostics; + +namespace Servus.Akka.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class ServusTraceExtensionsSpec : IDisposable +{ + private sealed class MockListener : IServusTraceListener + { + public bool IsEnabled(ServusTraceLevel level, ServusTraceCategory category) => true; + public void Write(in ServusTraceEvent evt) { } + } + + public void Dispose() + { + ServusTrace.Disable(); + } + + [Fact(Timeout = 5000)] + public void AddServusLoggerTracing_should_register_IServusTraceListener() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddServusLoggerTracing(); + + var provider = services.BuildServiceProvider(); + var listener = provider.GetService(); + + Assert.NotNull(listener); + Assert.IsType(listener); + } + + [Fact(Timeout = 5000)] + public void AddServusLoggerTracing_should_configure_ServusTrace_on_resolve() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddServusLoggerTracing(ServusTraceCategory.Connection); + + var provider = services.BuildServiceProvider(); + + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + + _ = provider.GetRequiredService(); + + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + } + + [Fact(Timeout = 5000)] + public void AddServusTraceListener_should_register_custom_listener_and_configure_ServusTrace() + { + var listener = new MockListener(); + var services = new ServiceCollection(); + services.AddServusTraceListener(listener); + + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + } + + [Fact(Timeout = 5000)] + public void AddServusTraceListener_should_throw_when_listener_is_null() + { + var services = new ServiceCollection(); + Assert.Throws(() => + services.AddServusTraceListener(null!)); + } + + [Fact(Timeout = 5000)] + public void AddServusLoggerTracing_should_respect_category_filter() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddServusLoggerTracing(ServusTraceCategory.Connection); + + var provider = services.BuildServiceProvider(); + _ = provider.GetRequiredService(); + + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Dns, ServusTraceLevel.Debug)); + } +} diff --git a/src/Servus.Akka.Tests/Diagnostics/ServusTraceSpec.cs b/src/Servus.Akka.Tests/Diagnostics/ServusTraceSpec.cs new file mode 100644 index 000000000..f6f164701 --- /dev/null +++ b/src/Servus.Akka.Tests/Diagnostics/ServusTraceSpec.cs @@ -0,0 +1,153 @@ +using System.Diagnostics; +using Servus.Akka.Diagnostics; + +namespace Servus.Akka.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class ServusTraceSpec : IDisposable +{ + private sealed class MockListener : IServusTraceListener + { + public List Events { get; } = []; + public bool IsEnabled(ServusTraceLevel level, ServusTraceCategory category) => true; + public void Write(in ServusTraceEvent evt) => Events.Add(evt); + } + + private readonly MockListener _mock = new(); + + public ServusTraceSpec() + { + ServusTrace.Disable(); + } + + public void Dispose() + { + ServusTrace.Disable(); + } + + [Fact(Timeout = 5000)] + public void ServusTraceEvent_FormatMessage_should_return_template_when_no_args() + { + var evt = new ServusTraceEvent( + Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, ServusTraceCategory.Connection, + "Test", 0, "Hello world"); + + Assert.Equal("Hello world", evt.FormatMessage()); + } + + [Fact(Timeout = 5000)] + public void ServusTraceEvent_FormatMessage_should_format_args_correctly() + { + var evt = new ServusTraceEvent( + Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, ServusTraceCategory.Pool, + "Test", 0, "Key={0} Value={1}", "host", 443); + + Assert.Equal("Key=host Value=443", evt.FormatMessage()); + } + + [Fact(Timeout = 5000)] + public void ShouldTrace_should_return_false_when_disabled() + { + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Pool, ServusTraceLevel.Error)); + } + + [Fact(Timeout = 5000)] + public void ShouldTrace_should_return_true_when_configured() + { + ServusTrace.Configure(_mock); + + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Pool, ServusTraceLevel.Warning)); + } + + [Fact(Timeout = 5000)] + public void ShouldTrace_should_respect_category_filter() + { + ServusTrace.Configure(_mock, ServusTraceCategory.Connection); + + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Dns, ServusTraceLevel.Debug)); + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Pool, ServusTraceLevel.Debug)); + } + + [Fact(Timeout = 5000)] + public void ShouldTrace_should_respect_minimum_level() + { + ServusTrace.Configure(_mock, ServusTraceCategory.All, ServusTraceLevel.Warning); + + Assert.False(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Debug)); + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Warning)); + Assert.True(ServusTrace.ShouldTrace(ServusTraceCategory.Connection, ServusTraceLevel.Error)); + } + + [Fact(Timeout = 5000)] + public void Connection_Debug_should_emit_event_when_configured() + { + ServusTrace.Configure(_mock); + + ServusTrace.Connection.Debug(this, "tcp connected to {0}:{1}", "localhost", 443); + + Assert.Single(_mock.Events); + var evt = _mock.Events[0]; + Assert.Equal(ServusTraceLevel.Debug, evt.Level); + Assert.Equal(ServusTraceCategory.Connection, evt.Category); + Assert.Equal(GetType().Name, evt.SourceType); + Assert.Equal("tcp connected to localhost:443", evt.FormatMessage()); + } + + [Fact(Timeout = 5000)] + public void Dns_Warning_should_emit_event_when_configured() + { + ServusTrace.Configure(_mock); + + ServusTrace.Dns.Warning(this, "DNS '{0}' failed: {1}", "badhost", "NXDOMAIN"); + + Assert.Single(_mock.Events); + var evt = _mock.Events[0]; + Assert.Equal(ServusTraceLevel.Warning, evt.Level); + Assert.Equal(ServusTraceCategory.Dns, evt.Category); + Assert.Equal("DNS 'badhost' failed: NXDOMAIN", evt.FormatMessage()); + } + + [Fact(Timeout = 5000)] + public void Tls_Debug_should_not_emit_when_category_not_enabled() + { + ServusTrace.Configure(_mock, ServusTraceCategory.Connection); + + ServusTrace.Tls.Debug(this, "TLS handshake starting"); + + Assert.Empty(_mock.Events); + } + + [Fact(Timeout = 5000)] + public void Pool_Debug_should_not_emit_when_disabled() + { + ServusTrace.Pool.Debug(this, "Establishing connection"); + + Assert.Empty(_mock.Events); + } + + [Fact(Timeout = 5000)] + public void Disable_should_stop_subsequent_trace_calls() + { + ServusTrace.Configure(_mock); + ServusTrace.Connection.Debug(this, "first event"); + ServusTrace.Disable(); + ServusTrace.Connection.Debug(this, "after disable"); + + Assert.Single(_mock.Events); + Assert.Equal("first event", _mock.Events[0].FormatMessage()); + } + + [Fact(Timeout = 5000)] + public void Connection_no_args_overload_should_emit_plain_message() + { + ServusTrace.Configure(_mock); + + ServusTrace.Connection.Debug(this, "connection disposed"); + + Assert.Single(_mock.Events); + Assert.Equal("connection disposed", _mock.Events[0].FormatMessage()); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.Tests/IO/AbruptCloseExceptionSpec.cs b/src/Servus.Akka.Tests/IO/AbruptCloseExceptionSpec.cs new file mode 100644 index 000000000..91f560ace --- /dev/null +++ b/src/Servus.Akka.Tests/IO/AbruptCloseExceptionSpec.cs @@ -0,0 +1,30 @@ +using Servus.Akka.IO; + +namespace Servus.Akka.Tests.IO; + +public sealed class AbruptCloseExceptionSpec +{ + [Fact(Timeout = 5000)] + public void AbruptCloseException_should_have_expected_message() + { + var ex = new AbruptCloseException(); + + Assert.Equal("Connection closed abruptly without close_notify", ex.Message); + } + + [Fact(Timeout = 5000)] + public void AbruptCloseException_should_derive_from_exception() + { + var ex = new AbruptCloseException(); + + Assert.IsAssignableFrom(ex); + } + + [Fact(Timeout = 5000)] + public void AbruptCloseException_should_have_null_inner_exception() + { + var ex = new AbruptCloseException(); + + Assert.Null(ex.InnerException); + } +} diff --git a/src/TurboHTTP.Tests/Transport/ClientByteMoverSpec.cs b/src/Servus.Akka.Tests/IO/ClientByteMoverSpec.cs similarity index 57% rename from src/TurboHTTP.Tests/Transport/ClientByteMoverSpec.cs rename to src/Servus.Akka.Tests/IO/ClientByteMoverSpec.cs index 52a9dbe5e..364e3d611 100644 --- a/src/TurboHTTP.Tests/Transport/ClientByteMoverSpec.cs +++ b/src/Servus.Akka.Tests/IO/ClientByteMoverSpec.cs @@ -1,8 +1,7 @@ using System.Threading.Channels; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class ClientByteMoverSpec { @@ -150,17 +149,14 @@ public async Task ClientByteMover_should_set_clean_close_on_eof() var inbound = Channel.CreateUnbounded(); var outbound = Channel.CreateUnbounded(); - // Empty stream (EOF immediately) var stream = new MemoryStream([], writable: false); var state = new ClientState(stream, inbound, outbound); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - // Act await ClientByteMover.MoveStreamToChannel(state, () => { }, cts.Token); - // Assert: clean close should be set - Assert.Equal(TlsCloseKind.CleanClose, state.CloseKind); + Assert.True(inbound.Reader.Completion.IsCompletedSuccessfully); } [Fact(Timeout = 5000)] @@ -169,17 +165,15 @@ public async Task ClientByteMover_should_set_abrupt_close_on_read_exception() var inbound = Channel.CreateUnbounded(); var outbound = Channel.CreateUnbounded(); - // Failing stream that throws on read var stream = new FailingStream(); var state = new ClientState(stream, inbound, outbound); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - // Act await ClientByteMover.MoveStreamToChannel(state, () => { }, cts.Token); - // Assert: abrupt close should be set - Assert.Equal(TlsCloseKind.AbruptClose, state.CloseKind); + Assert.True(inbound.Reader.Completion.IsFaulted); + Assert.IsType(inbound.Reader.Completion.Exception?.InnerException); } [Fact(Timeout = 5000)] @@ -218,10 +212,93 @@ public async Task ClientByteMover_should_handle_channel_to_stream_write_exceptio var inbound = Channel.CreateUnbounded(); var outbound = Channel.CreateUnbounded(); + var stream = new FailingStream(); + var state = new ClientState(stream, inbound, outbound); + + var buf = NetworkBuffer.Rent(10); + buf.Length = 10; + outbound.Writer.TryWrite(buf); + outbound.Writer.Complete(); + + var onCloseCalled = false; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveChannelToStream(state, () => { onCloseCalled = true; }, cts.Token); + + Assert.True(onCloseCalled); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_use_http3_factory_for_routed_buffers() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var stream = new MemoryStream([0xAB, 0xCD], writable: false); + var state = new ClientState(stream, inbound, outbound); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveStreamToChannel(state, () => { }, cts.Token, ClientByteMover.Http3Factory); + + var ok = inbound.Reader.TryRead(out var item); + Assert.True(ok); + Assert.IsType(item); + Assert.Equal(2, item.Length); + + item.Dispose(); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_handle_alternating_large_small_buffers() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var capturedWrites = new List(); + var stream = new CapturingStream(capturedWrites); + var state = new ClientState(stream, inbound, outbound); + + var largeBuf = NetworkBuffer.Rent(17 * 1024); + largeBuf.Memory.Span.Fill(0xAA); + largeBuf.Length = 17 * 1024; + + var smallBuf = NetworkBuffer.Rent(100); + smallBuf.Memory.Span.Fill(0xBB); + smallBuf.Length = 100; + + var largeBuf2 = NetworkBuffer.Rent(17 * 1024); + largeBuf2.Memory.Span.Fill(0xCC); + largeBuf2.Length = 17 * 1024; + + var smallBuf2 = NetworkBuffer.Rent(100); + smallBuf2.Memory.Span.Fill(0xDD); + smallBuf2.Length = 100; + + outbound.Writer.TryWrite(largeBuf); + outbound.Writer.TryWrite(smallBuf); + outbound.Writer.TryWrite(largeBuf2); + outbound.Writer.TryWrite(smallBuf2); + outbound.Writer.Complete(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveChannelToStream(state, () => { }, cts.Token); + + Assert.True(capturedWrites.Count >= 3); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_not_invoke_on_writes_complete_on_error() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var callbackInvoked = false; var stream = new FailingStream(); var state = new ClientState(stream, inbound, outbound) { - CloseKind = null + OnWritesComplete = () => { callbackInvoked = true; } }; var buf = NetworkBuffer.Rent(10); @@ -231,11 +308,109 @@ public async Task ClientByteMover_should_handle_channel_to_stream_write_exceptio using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - // Act await ClientByteMover.MoveChannelToStream(state, () => { }, cts.Token); - // Assert: close kind should be set to AbruptClose - Assert.Equal(TlsCloseKind.AbruptClose, state.CloseKind); + Assert.False(callbackInvoked); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_flush_coalesce_before_large_buffer() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var capturedWrites = new List(); + var stream = new CapturingStream(capturedWrites); + var state = new ClientState(stream, inbound, outbound); + + var smallBuf = NetworkBuffer.Rent(100); + smallBuf.Memory.Span.Fill(0x11); + smallBuf.Length = 100; + + var largeBuf = NetworkBuffer.Rent(17 * 1024); + largeBuf.Memory.Span.Fill(0xAA); + largeBuf.Length = 17 * 1024; + + outbound.Writer.TryWrite(smallBuf); + outbound.Writer.TryWrite(largeBuf); + outbound.Writer.Complete(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveChannelToStream(state, () => { }, cts.Token); + + Assert.True(capturedWrites.Count >= 2); + Assert.Equal(100, capturedWrites[0].Length); + Assert.Equal(17 * 1024, capturedWrites[1].Length); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_not_invoke_on_writes_complete_on_cancellation() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var callbackInvoked = false; + var stream = new SlowStream(); + var state = new ClientState(stream, inbound, outbound) + { + OnWritesComplete = () => { callbackInvoked = true; } + }; + + var buf = NetworkBuffer.Rent(10); + buf.Length = 10; + outbound.Writer.TryWrite(buf); + + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); + + await ClientByteMover.MoveChannelToStream(state, () => { }, cts.Token); + + Assert.False(callbackInvoked); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_handle_coalesce_buffer_overflow() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var capturedWrites = new List(); + var stream = new CapturingStream(capturedWrites); + var state = new ClientState(stream, inbound, outbound); + + for (var i = 0; i < 200; i++) + { + var smallBuf = NetworkBuffer.Rent(100); + smallBuf.Memory.Span.Fill((byte)(i % 256)); + smallBuf.Length = 100; + outbound.Writer.TryWrite(smallBuf); + } + + outbound.Writer.Complete(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveChannelToStream(state, () => { }, cts.Token); + + var totalBytes = capturedWrites.Sum(w => w.Length); + Assert.Equal(20_000, totalBytes); + } + + [Fact(Timeout = 5000)] + public async Task ClientByteMover_should_call_on_close_exactly_once_on_read_error() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var stream = new FailingStream(); + var state = new ClientState(stream, inbound, outbound); + + var closeCount = 0; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await ClientByteMover.MoveStreamToChannel(state, () => Interlocked.Increment(ref closeCount), cts.Token); + + Assert.Equal(1, closeCount); } private sealed class CapturingStream(List writes) : Stream @@ -268,6 +443,35 @@ public override void Flush() public override void SetLength(long value) => throw new NotSupportedException(); } + private sealed class SlowStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ct = default) + { + await Task.Delay(TimeSpan.FromSeconds(30), ct); + } + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } + private sealed class FailingStream : Stream { public override bool CanRead => true; diff --git a/src/TurboHTTP.Tests/Transport/ClientStateSpec.cs b/src/Servus.Akka.Tests/IO/ClientStateSpec.cs similarity index 72% rename from src/TurboHTTP.Tests/Transport/ClientStateSpec.cs rename to src/Servus.Akka.Tests/IO/ClientStateSpec.cs index 33bfd6471..648ab9768 100644 --- a/src/TurboHTTP.Tests/Transport/ClientStateSpec.cs +++ b/src/Servus.Akka.Tests/IO/ClientStateSpec.cs @@ -1,9 +1,9 @@ using System.Threading.Channels; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class ClientStateSpec { @@ -116,18 +116,6 @@ public void ClientState_should_expose_stream_property() Assert.Same(stream, state.Stream); } - [Fact(Timeout = 5000)] - public void ClientState_should_set_close_kind() - { - var stream = new MemoryStream(); - var state = new ClientState(stream, null, null); - - Assert.Null(state.CloseKind); - - state.CloseKind = TlsCloseKind.CleanClose; - Assert.Equal(TlsCloseKind.CleanClose, state.CloseKind); - } - [Fact(Timeout = 5000)] public void ClientState_should_allow_on_writes_complete_callback() { @@ -197,4 +185,84 @@ public void ClientState_should_handle_double_dispose() state.Dispose(); state.Dispose(); // Should not throw } + + [Fact(Timeout = 5000)] + public void ClientState_should_create_write_only_channels() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null, StreamDirection.WriteOnly); + + Assert.Equal(StreamDirection.WriteOnly, state.Direction); + Assert.NotNull(state.OutboundReader); + Assert.NotNull(state.OutboundWriter); + + var buf = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + Assert.True(state.OutboundWriter.TryWrite(buf)); + + Assert.False(state.InboundWriter.TryWrite(NetworkBufferTestExtensions.FromArray([4, 5, 6]))); + + state.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ClientState_should_create_read_only_channels() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null, StreamDirection.ReadOnly); + + Assert.Equal(StreamDirection.ReadOnly, state.Direction); + Assert.NotNull(state.InboundReader); + Assert.NotNull(state.InboundWriter); + + var buf = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + Assert.True(state.InboundWriter.TryWrite(buf)); + + Assert.False(state.OutboundWriter.TryWrite(NetworkBufferTestExtensions.FromArray([4, 5, 6]))); + + state.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ClientState_write_only_should_pre_complete_inbound_channel() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null, StreamDirection.WriteOnly); + + Assert.True(state.InboundReader.Completion.IsCompleted); + + state.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ClientState_read_only_should_pre_complete_outbound_channel() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null, StreamDirection.ReadOnly); + + Assert.True(state.OutboundReader.Completion.IsCompleted); + + state.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ClientState_should_default_to_bidirectional_direction() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null); + + Assert.Equal(StreamDirection.Bidirectional, state.Direction); + + state.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ClientState_should_expose_on_writes_complete_as_null_by_default() + { + var stream = new MemoryStream(); + var state = new ClientState(stream, null, null); + + Assert.Null(state.OnWritesComplete); + + state.Dispose(); + } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Transport/ConnectTunnelSpec.cs b/src/Servus.Akka.Tests/IO/ConnectTunnelSpec.cs similarity index 97% rename from src/TurboHTTP.Tests/Transport/ConnectTunnelSpec.cs rename to src/Servus.Akka.Tests/IO/ConnectTunnelSpec.cs index 0fc034b7d..469eef480 100644 --- a/src/TurboHTTP.Tests/Transport/ConnectTunnelSpec.cs +++ b/src/Servus.Akka.Tests/IO/ConnectTunnelSpec.cs @@ -2,9 +2,9 @@ using System.IO.Pipelines; using System.Net; using System.Text; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class ConnectTunnelSpec { @@ -70,7 +70,7 @@ public async Task Tunnel_should_throw_on_proxy_close() new SimpleProxy(), null, TestContext.Current.CancellationToken); await ReadRequestAsync(serverStream); - serverStream.Dispose(); + await serverStream.DisposeAsync(); await Assert.ThrowsAsync(() => tunnelTask); } @@ -161,8 +161,7 @@ public ICredentials? Credentials set { } } - public Uri? GetProxy(Uri destination) => - new Uri($"http://proxy.local:8080/"); + public Uri GetProxy(Uri destination) => new($"http://proxy.local:8080/"); public bool IsBypassed(Uri host) => false; } diff --git a/src/TurboHTTP.Tests/Transport/ConnectionHandleSpec.cs b/src/Servus.Akka.Tests/IO/ConnectionHandleSpec.cs similarity index 70% rename from src/TurboHTTP.Tests/Transport/ConnectionHandleSpec.cs rename to src/Servus.Akka.Tests/IO/ConnectionHandleSpec.cs index 19a989bae..8cc5fe2cd 100644 --- a/src/TurboHTTP.Tests/Transport/ConnectionHandleSpec.cs +++ b/src/Servus.Akka.Tests/IO/ConnectionHandleSpec.cs @@ -1,10 +1,9 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class ConnectionHandleSpec { @@ -203,4 +202,98 @@ public void ConnectionActor_property_should_be_set() Assert.Equal(ActorRefs.Nobody, handle.ConnectionActor); } + + [Fact(Timeout = 5000)] + public void Equals_should_return_false_for_null() + { + var handle = CreateHandle(); + + Assert.False(handle.Equals(null)); + } + + [Fact(Timeout = 5000)] + public void Equals_should_return_true_for_same_instance() + { + var handle = CreateHandle(); + + Assert.True(handle.Equals(handle)); + } + + [Fact(Timeout = 5000)] + public void Equals_should_return_false_for_different_key() + { + var outbound = Channel.CreateUnbounded(); + var inbound = Channel.CreateUnbounded(); + + var key1 = new RequestEndpoint + { + Host = "host-a", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version20 + }; + var key2 = new RequestEndpoint + { + Host = "host-b", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version20 + }; + + var handle1 = new ConnectionHandle(outbound.Writer, inbound.Reader, key1, ActorRefs.Nobody); + var handle2 = new ConnectionHandle(outbound.Writer, inbound.Reader, key2, ActorRefs.Nobody); + + Assert.NotEqual(handle1, handle2); + } + + [Fact(Timeout = 5000)] + public void GetHashCode_should_be_consistent() + { + var handle = CreateHandle(); + + var hash1 = handle.GetHashCode(); + var hash2 = handle.GetHashCode(); + + Assert.Equal(hash1, hash2); + } + + [Fact(Timeout = 5000)] + public void UpdateMaxConcurrentStreams_should_accept_zero() + { + var handle = CreateHandle(); + + handle.UpdateMaxConcurrentStreams(0); + + Assert.Equal(0, handle.MaxConcurrentStreams); + } + + [Fact(Timeout = 5000)] + public void UpdateMaxConcurrentStreams_should_accept_max_value() + { + var handle = CreateHandle(); + + handle.UpdateMaxConcurrentStreams(int.MaxValue); + + Assert.Equal(int.MaxValue, handle.MaxConcurrentStreams); + } + + [Fact(Timeout = 5000)] + public void Equality_operator_should_match_equals() + { + var outbound = Channel.CreateUnbounded(); + var inbound = Channel.CreateUnbounded(); + var key = new RequestEndpoint + { + Host = "localhost", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version20 + }; + + var handle1 = new ConnectionHandle(outbound.Writer, inbound.Reader, key, ActorRefs.Nobody); + var handle2 = new ConnectionHandle(outbound.Writer, inbound.Reader, key, ActorRefs.Nobody); + + Assert.True(handle1 == handle2); + Assert.False(handle1 != handle2); + } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Transport/ConnectionLeaseSpec.cs b/src/Servus.Akka.Tests/IO/ConnectionLeaseSpec.cs similarity index 81% rename from src/TurboHTTP.Tests/Transport/ConnectionLeaseSpec.cs rename to src/Servus.Akka.Tests/IO/ConnectionLeaseSpec.cs index cfac32f8d..3f9b2a908 100644 --- a/src/TurboHTTP.Tests/Transport/ConnectionLeaseSpec.cs +++ b/src/Servus.Akka.Tests/IO/ConnectionLeaseSpec.cs @@ -1,10 +1,9 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class ConnectionLeaseSpec { @@ -508,4 +507,115 @@ public async Task Token_should_allow_waiting_for_disposal() await disposeTask; Assert.True(token.IsCancellationRequested); } + + [Fact(Timeout = 5000)] + public async Task IsExpired_should_consider_zero_timespan_as_expired_after_tick() + { + var handle = CreateHandle(HttpVersion.Version11); + using var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + await Task.Delay(2, TestContext.Current.CancellationToken); + Assert.True(lease.IsExpired(TimeSpan.Zero)); + } + + [Fact(Timeout = 5000)] + public void IsExpired_should_treat_minus_one_ms_as_infinite() + { + var handle = CreateHandle(HttpVersion.Version11); + using var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + // TimeSpan.FromMilliseconds(-1) == Timeout.InfiniteTimeSpan + Assert.False(lease.IsExpired(TimeSpan.FromMilliseconds(-1))); + } + + [Fact(Timeout = 5000)] + public void MaxConcurrentStreams_should_default_to_100_for_unknown_major_version() + { + var handle = CreateHandle(new Version(4, 0)); + using var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + Assert.Equal(100, lease.MaxConcurrentStreams); + } + + [Fact(Timeout = 5000)] + public void MaxConcurrentStreams_should_default_to_6_for_http11_minor_variants() + { + var handle = CreateHandle(new Version(1, 2)); + using var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + Assert.Equal(6, lease.MaxConcurrentStreams); + } + + [Fact(Timeout = 5000)] + public void HasAvailableSlot_should_be_false_at_exact_capacity_boundary() + { + var handle = CreateHandle(HttpVersion.Version20); + using var state = CreateState(); + var lease = new ConnectionLease(handle, state); + lease.UpdateMaxConcurrentStreams(3); + + lease.MarkBusy(); + lease.MarkBusy(); + Assert.True(lease.HasAvailableSlot); + + lease.MarkBusy(); + Assert.False(lease.HasAvailableSlot); + } + + [Fact(Timeout = 5000)] + public void MarkBusy_after_dispose_should_not_throw() + { + var handle = CreateHandle(HttpVersion.Version11); + var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + lease.Dispose(); + lease.MarkBusy(); + + Assert.Equal(1, lease.ActiveStreams); + } + + [Fact(Timeout = 5000)] + public void MarkIdle_after_dispose_should_not_throw() + { + var handle = CreateHandle(HttpVersion.Version11); + var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + lease.MarkBusy(); + lease.Dispose(); + lease.MarkIdle(); + + Assert.Equal(0, lease.ActiveStreams); + } + + [Fact(Timeout = 5000)] + public void MarkNoReuse_after_dispose_should_not_throw() + { + var handle = CreateHandle(HttpVersion.Version11); + var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + lease.Dispose(); + lease.MarkNoReuse(); + + Assert.False(lease.Reusable); + } + + [Fact(Timeout = 5000)] + public void UpdateMaxConcurrentStreams_after_dispose_should_not_throw() + { + var handle = CreateHandle(HttpVersion.Version11); + var state = CreateState(); + var lease = new ConnectionLease(handle, state); + + lease.Dispose(); + lease.UpdateMaxConcurrentStreams(50); + + Assert.Equal(50, lease.MaxConcurrentStreams); + } } \ No newline at end of file diff --git a/src/TurboHTTP.StreamTests/Transport/ConnectionPoolDeadlockSpec.cs b/src/Servus.Akka.Tests/IO/ConnectionPoolDeadlockSpec.cs similarity index 96% rename from src/TurboHTTP.StreamTests/Transport/ConnectionPoolDeadlockSpec.cs rename to src/Servus.Akka.Tests/IO/ConnectionPoolDeadlockSpec.cs index 420cef6f3..e39a41898 100644 --- a/src/TurboHTTP.StreamTests/Transport/ConnectionPoolDeadlockSpec.cs +++ b/src/Servus.Akka.Tests/IO/ConnectionPoolDeadlockSpec.cs @@ -1,13 +1,14 @@ using System.Net; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; +using Akka.TestKit.Xunit; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO; -public sealed class ConnectionPoolDeadlockSpec : StreamTestBase +public sealed class ConnectionPoolDeadlockSpec : TestKit { private readonly InMemoryConnectionFactory _factory = new(); diff --git a/src/TurboHTTP.Tests/Transport/GenerationCounterSpec.cs b/src/Servus.Akka.Tests/IO/GenerationCounterSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/GenerationCounterSpec.cs rename to src/Servus.Akka.Tests/IO/GenerationCounterSpec.cs index 09de4bb5c..32db1b989 100644 --- a/src/TurboHTTP.Tests/Transport/GenerationCounterSpec.cs +++ b/src/Servus.Akka.Tests/IO/GenerationCounterSpec.cs @@ -1,6 +1,6 @@ using System.Threading.Channels; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO; public sealed class GenerationCounterSpec { diff --git a/src/Servus.Akka.Tests/IO/MessagesSpec.cs b/src/Servus.Akka.Tests/IO/MessagesSpec.cs new file mode 100644 index 000000000..d12472fe4 --- /dev/null +++ b/src/Servus.Akka.Tests/IO/MessagesSpec.cs @@ -0,0 +1,301 @@ +using System.Net; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; + +namespace Servus.Akka.Tests.IO; + +public sealed class MessagesSpec +{ + private static readonly RequestEndpoint TestKey = new() + { + Scheme = "https", + Host = "localhost", + Port = 443, + Version = HttpVersion.Version20 + }; + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Rent_should_return_buffer_with_capacity() + { + var buf = NetworkBuffer.Rent(1024); + + Assert.True(buf.Capacity >= 1024); + Assert.Equal(0, buf.Length); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Rent_should_have_key() + { + var buf = NetworkBuffer.Rent(64); + + Assert.Equal(string.Empty, buf.Key.Host); + Assert.Equal(string.Empty, buf.Key.Scheme); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_should_expose_memory_up_to_length() + { + var buf = NetworkBuffer.Rent(256); + buf.Length = 10; + + Assert.Equal(10, buf.Memory.Length); + Assert.Equal(10, buf.Span.Length); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_should_expose_full_memory() + { + var buf = NetworkBuffer.Rent(256); + buf.Length = 10; + + Assert.True(buf.FullMemory.Length >= 256); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Dispose_should_be_idempotent() + { + var buf = NetworkBuffer.Rent(64); + + buf.Dispose(); + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Capacity_should_be_zero_after_dispose() + { + var buf = NetworkBuffer.Rent(64); + + buf.Dispose(); + + Assert.Equal(0, buf.Capacity); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Key_should_be_settable() + { + var buf = NetworkBuffer.Rent(64); + buf.Key = TestKey; + + Assert.Equal(TestKey, buf.Key); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_Length_should_be_settable() + { + var buf = NetworkBuffer.Rent(256); + buf.Length = 128; + + Assert.Equal(128, buf.Length); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void RoutedNetworkBuffer_Rent_should_return_buffer_with_null_stream_fields() + { + var buf = RoutedNetworkBuffer.Rent(1024); + + Assert.Null(buf.StreamTypeValue); + Assert.Null(buf.StreamId); + Assert.True(buf.Capacity >= 1024); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void RoutedNetworkBuffer_should_allow_setting_stream_fields() + { + var buf = RoutedNetworkBuffer.Rent(64); + buf.StreamTypeValue = 0x00; + buf.StreamId = 42; + + Assert.Equal(0x00, buf.StreamTypeValue); + Assert.Equal(42, buf.StreamId); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void RoutedNetworkBuffer_Dispose_should_be_idempotent() + { + var buf = RoutedNetworkBuffer.Rent(64); + + buf.Dispose(); + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void RoutedNetworkBuffer_Capacity_should_be_zero_after_dispose() + { + var buf = RoutedNetworkBuffer.Rent(64); + + buf.Dispose(); + + Assert.Equal(0, buf.Capacity); + } + + [Fact(Timeout = 5000)] + public void ConnectionReuseItem_should_preserve_fields() + { + var item = new ConnectionReuseItem(true) { Key = TestKey }; + + Assert.True(item.CanReuse); + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void ConnectionReuseItem_equality_should_compare_all_fields() + { + var a = new ConnectionReuseItem(true) { Key = TestKey }; + var b = new ConnectionReuseItem(true) { Key = TestKey }; + var c = new ConnectionReuseItem(false) { Key = TestKey }; + + Assert.Equal(a, b); + Assert.NotEqual(a, c); + } + + [Fact(Timeout = 5000)] + public void ConnectItem_should_preserve_fields() + { + var opts = new TcpOptions { Host = "localhost", Port = 443 }; + var item = new ConnectItem(opts) { Key = TestKey, IsReconnect = true }; + + Assert.Same(opts, item.Options); + Assert.Equal(TestKey, item.Key); + Assert.True(item.IsReconnect); + } + + [Fact(Timeout = 5000)] + public void ConnectItem_IsReconnect_should_default_to_false() + { + var opts = new TcpOptions { Host = "localhost", Port = 443 }; + var item = new ConnectItem(opts) { Key = TestKey }; + + Assert.False(item.IsReconnect); + } + + [Fact(Timeout = 5000)] + public void MaxConcurrentStreamsItem_should_preserve_fields() + { + var item = new MaxConcurrentStreamsItem(42) { Key = TestKey }; + + Assert.Equal(42, item.MaxStreams); + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void StreamAcquireItem_should_preserve_key() + { + var item = new StreamAcquireItem { Key = TestKey }; + + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void CloseSignalItem_should_preserve_fields() + { + var item = new CloseSignalItem(TlsCloseKind.AbruptClose) { Key = TestKey }; + + Assert.Equal(TlsCloseKind.AbruptClose, item.CloseKind); + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void ConnectedSignalItem_should_preserve_key() + { + var item = new ConnectedSignalItem { Key = TestKey }; + + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void TlsCloseKind_should_have_expected_values() + { + Assert.Equal(0, (int)TlsCloseKind.CleanClose); + Assert.Equal(1, (int)TlsCloseKind.AbruptClose); + } + + [Fact(Timeout = 5000)] + public void QuicCloseKind_should_have_expected_values() + { + Assert.Equal(0, (int)QuicCloseKind.RequestStreamComplete); + Assert.Equal(1, (int)QuicCloseKind.ConnectionFailure); + Assert.Equal(2, (int)QuicCloseKind.MigrationDisallowed); + Assert.Equal(3, (int)QuicCloseKind.WriteFailed); + Assert.Equal(4, (int)QuicCloseKind.AcquisitionFailed); + } + + [Fact(Timeout = 5000)] + public void QuicCloseItem_should_preserve_fields() + { + var item = new QuicCloseItem(QuicCloseKind.ConnectionFailure, 7) { Key = TestKey }; + + Assert.Equal(QuicCloseKind.ConnectionFailure, item.Kind); + Assert.Equal(7, item.StreamId); + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void QuicCloseItem_StreamId_should_default_to_minus_one() + { + var item = new QuicCloseItem(QuicCloseKind.RequestStreamComplete); + + Assert.Equal(-1, item.StreamId); + } + + [Fact(Timeout = 5000)] + public void OpenTypedStreamItem_should_preserve_fields() + { + var item = new OpenTypedStreamItem(0x00, -2, true) { Key = TestKey }; + + Assert.Equal(0x00, item.StreamTypeValue); + Assert.Equal(-2, item.SyntheticStreamId); + Assert.True(item.Outbound); + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void Http3EndOfRequestItem_should_preserve_fields() + { + var item = new Http3EndOfRequestItem { Key = TestKey, StreamId = 99 }; + + Assert.Equal(TestKey, item.Key); + Assert.Equal(99, item.StreamId); + } + + [Fact(Timeout = 5000)] + public void ProtocolReadyItem_should_preserve_key() + { + var item = new ProtocolReadyItem { Key = TestKey }; + + Assert.Equal(TestKey, item.Key); + } + + [Fact(Timeout = 5000)] + public void NetworkBuffer_ConfigurePoolSize_should_update_pool() + { + var original = Environment.ProcessorCount * 2; + try + { + NetworkBuffer.ConfigurePoolSize(4); + + var buf = NetworkBuffer.Rent(64); + buf.Dispose(); + } + finally + { + NetworkBuffer.ConfigurePoolSize(original); + } + } +} diff --git a/src/TurboHTTP.Tests/Transport/QuicClientProviderSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicClientProviderSpec.cs similarity index 98% rename from src/TurboHTTP.Tests/Transport/QuicClientProviderSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicClientProviderSpec.cs index 42b761829..27a8776e8 100644 --- a/src/TurboHTTP.Tests/Transport/QuicClientProviderSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicClientProviderSpec.cs @@ -1,6 +1,6 @@ -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Quic; #pragma warning disable CA1416 diff --git a/src/Servus.Akka.Tests/IO/Quic/QuicConnectionFactorySpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionFactorySpec.cs new file mode 100644 index 000000000..679acb54e --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionFactorySpec.cs @@ -0,0 +1,23 @@ +using Servus.Akka.IO.Quic; + +#pragma warning disable CA1416 + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class QuicConnectionFactorySpec +{ + [Fact(Timeout = 5000)] + public void Instance_should_be_singleton() + { + var instance1 = QuicConnectionFactory.Instance; + var instance2 = QuicConnectionFactory.Instance; + + Assert.Same(instance1, instance2); + } + + [Fact(Timeout = 5000)] + public void Instance_should_not_be_null() + { + Assert.NotNull(QuicConnectionFactory.Instance); + } +} diff --git a/src/TurboHTTP.Tests/Transport/QuicConnectionHandleSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionHandleSpec.cs similarity index 70% rename from src/TurboHTTP.Tests/Transport/QuicConnectionHandleSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicConnectionHandleSpec.cs index faf651cf6..97c2d227c 100644 --- a/src/TurboHTTP.Tests/Transport/QuicConnectionHandleSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionHandleSpec.cs @@ -1,11 +1,11 @@ using System.Net; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; #pragma warning disable CA1416 -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicConnectionHandleSpec { @@ -68,66 +68,42 @@ public async Task QuicConnectionHandle_should_open_stream_as_lease_for_request_s var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); Assert.NotNull(lease); Assert.True(lease.IsAlive); } [Fact(Timeout = 5000)] - public async Task QuicConnectionHandle_should_open_stream_as_lease_for_control_streams() + public async Task QuicConnectionHandle_should_open_stream_as_lease_for_unidirectional_streams() { var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); Assert.NotNull(lease); Assert.True(lease.IsAlive); } - [Fact(Timeout = 5000)] - public async Task QuicConnectionHandle_should_open_stream_as_lease_for_qpack_encoder() - { - var provider = new FakeClientProvider(); - var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.QpackEncoder, - TestContext.Current.CancellationToken); - - Assert.NotNull(lease); - Assert.True(lease.IsAlive); - } - - [Fact(Timeout = 5000)] - public async Task QuicConnectionHandle_should_throw_for_qpack_decoder_as_output() - { - var provider = new FakeClientProvider(); - var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - - // QpackDecoder is receive-only, not supported for opening - await Assert.ThrowsAsync(async () => - await handle.OpenStreamAsLeaseAsync(Http3StreamType.QpackDecoder, TestContext.Current.CancellationToken)); - } - [Fact(Timeout = 5000)] public async Task QuicConnectionHandle_opened_request_stream_should_have_correct_stream_type() { var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); Assert.NotNull(lease); } [Fact(Timeout = 5000)] - public async Task QuicConnectionHandle_opened_control_stream_should_be_usable() + public async Task QuicConnectionHandle_opened_unidirectional_stream_should_be_usable() { var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); Assert.NotNull(lease); } @@ -138,7 +114,7 @@ public async Task QuicConnectionHandle_opened_stream_lease_should_reference_hand var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); Assert.Equal(TestEndpoint, lease.Key); } @@ -185,12 +161,12 @@ public async Task QuicConnectionHandle_inbound_stream_record_encapsulates_lease_ { var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); - var inboundStream = new QuicConnectionHandle.InboundStream(lease, Http3StreamType.Control); + var inboundStream = new QuicConnectionHandle.InboundStream(lease, 0x00, 3); Assert.Same(lease, inboundStream.Lease); - Assert.Equal(Http3StreamType.Control, inboundStream.StreamType); + Assert.Equal(0x00, inboundStream.StreamTypeValue); } [Fact(Timeout = 5000)] @@ -198,12 +174,12 @@ public async Task QuicConnectionHandle_inbound_stream_record_equality_based_on_l { var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); - var stream1 = new QuicConnectionHandle.InboundStream(lease, Http3StreamType.Control); - var stream2 = new QuicConnectionHandle.InboundStream(lease, Http3StreamType.Control); + var stream1 = new QuicConnectionHandle.InboundStream(lease, 0x00, 3); + var stream2 = new QuicConnectionHandle.InboundStream(lease, 0x00, 3); - // Records with same lease and stream type should be equal + // Records with same lease and stream type value should be equal Assert.Equal(stream1, stream2); } @@ -213,7 +189,7 @@ public async Task QuicConnectionHandle_opened_stream_lease_should_have_client_st var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); // Stream lease should have a valid ClientState Assert.NotNull(lease.State); @@ -225,9 +201,9 @@ public async Task QuicConnectionHandle_opened_stream_lease_should_have_key_set() var provider = new FakeClientProvider(); var handle = new QuicConnectionHandle(provider, TestOptions, TestEndpoint); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, TestContext.Current.CancellationToken); + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); // Stream lease should preserve the endpoint key Assert.Equal(TestEndpoint, lease.Key); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Transport/QuicConnectionLeaseSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionLeaseSpec.cs similarity index 96% rename from src/TurboHTTP.Tests/Transport/QuicConnectionLeaseSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicConnectionLeaseSpec.cs index 58f5e8313..7947ae763 100644 --- a/src/TurboHTTP.Tests/Transport/QuicConnectionLeaseSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionLeaseSpec.cs @@ -1,11 +1,11 @@ using System.Net; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; #pragma warning disable CA1416 -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicConnectionLeaseSpec { diff --git a/src/TurboHTTP.StreamTests/Transport/QuicConnectionManagerActorSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerActorSpec.cs similarity index 84% rename from src/TurboHTTP.StreamTests/Transport/QuicConnectionManagerActorSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerActorSpec.cs index 587e3e2f8..0a8b0dbde 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicConnectionManagerActorSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerActorSpec.cs @@ -1,14 +1,15 @@ using System.Net; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Akka.TestKit.Xunit; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; #pragma warning disable CA1416 -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; -public sealed class QuicConnectionManagerActorSpec : StreamTestBase +public sealed class QuicConnectionManagerActorSpec : TestKit { private readonly InMemoryQuicConnectionFactory _factory = new(); @@ -249,12 +250,18 @@ public async Task Multiple_hosts_should_be_independent() var options1 = new QuicOptions { Host = "host1.example.com", Port = 443 }; var endpoint1 = new RequestEndpoint { - Host = "host1.example.com", Port = 443, Scheme = "https", Version = HttpVersion.Version30 + Host = "host1.example.com", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version30 }; var options2 = new QuicOptions { Host = "host2.example.com", Port = 443 }; var endpoint2 = new RequestEndpoint { - Host = "host2.example.com", Port = 443, Scheme = "https", Version = HttpVersion.Version30 + Host = "host2.example.com", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version30 }; var lease1 = @@ -357,7 +364,10 @@ public async Task Release_unknown_lease_should_dispose() var handle = new QuicConnectionHandle(provider, new QuicOptions { Host = "orphan.local", Port = 443 }, new RequestEndpoint { - Host = "orphan.local", Port = 443, Scheme = "https", Version = HttpVersion.Version30 + Host = "orphan.local", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version30 }); var orphanLease = new QuicConnectionLease(handle); orphanLease.MarkBusy(); @@ -421,12 +431,18 @@ public async Task Multiple_hosts_should_maintain_separate_pools() var options1 = new QuicOptions { Host = "host1.example.com", Port = 443 }; var endpoint1 = new RequestEndpoint { - Host = "host1.example.com", Port = 443, Scheme = "https", Version = HttpVersion.Version30 + Host = "host1.example.com", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version30 }; var options2 = new QuicOptions { Host = "host2.example.com", Port = 443 }; var endpoint2 = new RequestEndpoint { - Host = "host2.example.com", Port = 443, Scheme = "https", Version = HttpVersion.Version30 + Host = "host2.example.com", + Port = 443, + Scheme = "https", + Version = HttpVersion.Version30 }; var lease1 = @@ -475,6 +491,59 @@ await QuicConnectionManagerActor.AcquireAsync(actor, options, endpoint, actor.Tell(new QuicConnectionManagerActor.Release(lease1, CanReuse: false)); } + [Fact(Timeout = 5000)] + public async Task Acquire_with_already_cancelled_token_should_be_ignored_by_actor() + { + var actor = CreateActor(); + var options = CreateOptions(); + var endpoint = CreateEndpoint(); + + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + // UnsafeRegister fires synchronously for already-cancelled tokens, so TCS is completed + // before actor.Tell. The actor receives a completed TCS and immediately returns. + await Assert.ThrowsAnyAsync(() => + QuicConnectionManagerActor.AcquireAsync(actor, options, endpoint, cts.Token)); + + // Actor must still be alive and functional + var lease = await QuicConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + Assert.NotNull(lease); + + actor.Tell(new QuicConnectionManagerActor.Release(lease, CanReuse: false)); + } + + [Fact(Timeout = 5000)] + public async Task Established_with_cancelled_caller_should_release_back_to_pool() + { + var slowFactory = new SlowQuicConnectionFactory(TimeSpan.FromMilliseconds(200)); + var actor = Sys.ActorOf(Props.Create(() => + new QuicConnectionManagerActor(slowFactory, TimeSpan.FromSeconds(30), Timeout.InfiniteTimeSpan, + maxConnectionsPerHost: 1))); + var options = CreateOptions(); + var endpoint = CreateEndpoint(); + + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(30)); + + // acquire1: cancelled before factory completes; establishing slot held + var task1 = QuicConnectionManagerActor.AcquireAsync(actor, options, endpoint, cts.Token); + + // acquire2: queued (max=1, establishing=1) → served after OnEstablished cascades via OnRelease + var task2 = QuicConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + + await Assert.ThrowsAnyAsync(() => task1); + + // When factory resolves, OnEstablished → TrySetResult(tcs1) fails → + // OnRelease → pending queue → ServeNextPending / direct handoff for task2 + var lease = await task2; + Assert.NotNull(lease); + Assert.True(lease.IsAlive); + + actor.Tell(new QuicConnectionManagerActor.Release(lease, CanReuse: false)); + } + [Fact(Timeout = 5000)] public async Task Evicted_idle_connection_should_not_be_reused() { diff --git a/src/TurboHTTP.Tests/Transport/QuicConnectionManagerSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerSpec.cs similarity index 69% rename from src/TurboHTTP.Tests/Transport/QuicConnectionManagerSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerSpec.cs index d21bf9eb1..c7dbb9324 100644 --- a/src/TurboHTTP.Tests/Transport/QuicConnectionManagerSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionManagerSpec.cs @@ -1,11 +1,10 @@ -using TurboHTTP.Internal; -using TurboHTTP.Protocol.Http3; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; #pragma warning disable CA1416 -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicConnectionManagerSpec { @@ -32,7 +31,7 @@ public async Task QuicConnectionHandle_should_return_live_lease_when_opening_req var provider = new FakeClientProvider(); await using var handle = CreateHandle(provider); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); Assert.NotNull(lease); @@ -48,7 +47,7 @@ public async Task QuicConnectionHandle_should_return_live_lease_when_opening_con var provider = new FakeClientProvider(); await using var handle = CreateHandle(provider); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); Assert.NotNull(lease); @@ -63,7 +62,7 @@ public async Task QuicConnectionHandle_should_return_live_lease_when_opening_qpa var provider = new FakeClientProvider(); await using var handle = CreateHandle(provider); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.QpackEncoder, + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); Assert.NotNull(lease); @@ -79,9 +78,9 @@ public async Task QuicConnectionHandle_should_reuse_provider_across_multiple_str await using var handle = CreateHandle(provider); var lease1 = - await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken); + await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); var lease2 = - await handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, TestContext.Current.CancellationToken); + await handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken); Assert.True(lease1.IsAlive); Assert.True(lease2.IsAlive); @@ -99,9 +98,9 @@ public async Task QuicConnectionHandle_should_open_streams_concurrently_without_ var tasks = new[] { - handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, TestContext.Current.CancellationToken), - handle.OpenStreamAsLeaseAsync(Http3StreamType.Control, TestContext.Current.CancellationToken), - handle.OpenStreamAsLeaseAsync(Http3StreamType.QpackEncoder, TestContext.Current.CancellationToken), + handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken), + handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken), + handle.OpenStreamAsLeaseAsync(bidirectional: false, TestContext.Current.CancellationToken), }; var leases = await Task.WhenAll(tasks); @@ -126,19 +125,21 @@ public async Task QuicConnectionHandle_should_throw_operation_canceled_when_open await cts.CancelAsync(); await Assert.ThrowsAnyAsync(() => - handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, cts.Token)); + handle.OpenStreamAsLeaseAsync(bidirectional: true, cts.Token)); } [Fact(Timeout = 5000)] - public async Task QuicConnectionHandle_should_return_null_for_unknown_inbound_stream_type() + public async Task QuicConnectionHandle_should_pass_through_unknown_inbound_stream_type() { - // An inbound stream whose varint identifies an unknown type should be discarded (returns null). - var provider = new FakeClientProvider(inboundBytes: [0xFF, 0x00]); // unrecognized stream type + var provider = new FakeClientProvider(inboundBytes: [0xFF, 0x00]); await using var handle = CreateHandle(provider); var result = await handle.AcceptInboundStreamAsLeaseAsync(TestContext.Current.CancellationToken); - Assert.Null(result); + Assert.NotNull(result); + Assert.Equal(0xFF, result.StreamTypeValue); + + result.Lease.Dispose(); } [Fact(Timeout = 5000)] @@ -147,12 +148,12 @@ public async Task InboundStream_record_should_hold_lease_and_stream_type() var provider = new FakeClientProvider(); await using var handle = CreateHandle(provider); - var lease = await handle.OpenStreamAsLeaseAsync(Http3StreamType.Request, + var lease = await handle.OpenStreamAsLeaseAsync(bidirectional: true, TestContext.Current.CancellationToken); - var inbound = new QuicConnectionHandle.InboundStream(lease, Http3StreamType.Control); + var inbound = new QuicConnectionHandle.InboundStream(lease, 0x00, 3); Assert.Same(lease, inbound.Lease); - Assert.Equal(Http3StreamType.Control, inbound.StreamType); + Assert.Equal(0x00, inbound.StreamTypeValue); lease.Dispose(); } @@ -160,16 +161,13 @@ public async Task InboundStream_record_should_hold_lease_and_stream_type() [Fact(Timeout = 5000)] public async Task AcceptInboundStreamAsLeaseAsync_should_return_control_stream() { - var controlVarint = new byte[1]; - QuicVarInt.Encode((long)StreamType.Control, controlVarint); - - var provider = new FakeClientProvider(inboundBytes: controlVarint); + var provider = new FakeClientProvider(inboundBytes: [0x00]); await using var handle = CreateHandle(provider); var result = await handle.AcceptInboundStreamAsLeaseAsync(TestContext.Current.CancellationToken); Assert.NotNull(result); - Assert.Equal(Http3StreamType.Control, result.StreamType); + Assert.Equal(0x00, result.StreamTypeValue); Assert.True(result.Lease.IsAlive); result.Lease.Dispose(); @@ -178,16 +176,13 @@ public async Task AcceptInboundStreamAsLeaseAsync_should_return_control_stream() [Fact(Timeout = 5000)] public async Task AcceptInboundStreamAsLeaseAsync_should_return_qpack_encoder_stream() { - var varint = new byte[1]; - QuicVarInt.Encode((long)StreamType.QpackEncoder, varint); - - var provider = new FakeClientProvider(inboundBytes: varint); + var provider = new FakeClientProvider(inboundBytes: [0x02]); await using var handle = CreateHandle(provider); var result = await handle.AcceptInboundStreamAsLeaseAsync(TestContext.Current.CancellationToken); Assert.NotNull(result); - Assert.Equal(Http3StreamType.QpackEncoder, result.StreamType); + Assert.Equal(0x02, result.StreamTypeValue); result.Lease.Dispose(); } @@ -195,16 +190,13 @@ public async Task AcceptInboundStreamAsLeaseAsync_should_return_qpack_encoder_st [Fact(Timeout = 5000)] public async Task AcceptInboundStreamAsLeaseAsync_should_return_qpack_decoder_stream() { - var varint = new byte[1]; - QuicVarInt.Encode((long)StreamType.QpackDecoder, varint); - - var provider = new FakeClientProvider(inboundBytes: varint); + var provider = new FakeClientProvider(inboundBytes: [0x03]); await using var handle = CreateHandle(provider); var result = await handle.AcceptInboundStreamAsLeaseAsync(TestContext.Current.CancellationToken); Assert.NotNull(result); - Assert.Equal(Http3StreamType.QpackDecoder, result.StreamType); + Assert.Equal(0x03, result.StreamTypeValue); result.Lease.Dispose(); } @@ -234,16 +226,6 @@ public async Task AcceptInboundStreamAsLeaseAsync_should_return_null_when_cancel Assert.Null(result); } - [Fact(Timeout = 5000)] - public async Task OpenStreamAsLeaseAsync_should_throw_for_unknown_type() - { - var provider = new FakeClientProvider(); - await using var handle = CreateHandle(provider); - - await Assert.ThrowsAsync(() => - handle.OpenStreamAsLeaseAsync(Http3StreamType.QpackDecoder, TestContext.Current.CancellationToken)); - } - [Fact(Timeout = 5000)] public async Task DisposeAsync_should_dispose_provider() { @@ -254,4 +236,4 @@ public async Task DisposeAsync_should_dispose_provider() Assert.True(provider.Disposed); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.StreamTests/Transport/QuicConnectionStageSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionStageSpec.cs similarity index 68% rename from src/TurboHTTP.StreamTests/Transport/QuicConnectionStageSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicConnectionStageSpec.cs index 01157f620..cac2634a5 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicConnectionStageSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicConnectionStageSpec.cs @@ -1,7 +1,7 @@ using Akka.Actor; -using TurboHTTP.Transport.Quic; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicConnectionStageSpec { @@ -10,7 +10,6 @@ public void Stage_should_create_successfully() { var stage = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: true); Assert.NotNull(stage); @@ -22,7 +21,6 @@ public void Stage_should_have_inlet_and_outlet() { var stage = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: true); var shape = stage.Shape; @@ -35,7 +33,6 @@ public void Stage_with_migration_disabled_should_initialize() { var stage = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: false); Assert.NotNull(stage); @@ -48,7 +45,6 @@ public void Stage_should_support_multiple_instantiation() { var stage = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: true); Assert.NotNull(stage); @@ -60,7 +56,6 @@ public void Stage_shape_inlet_outlet_not_null() { var stage = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: true); var shape = stage.Shape; @@ -71,26 +66,16 @@ public void Stage_shape_inlet_outlet_not_null() [Fact(Timeout = 5000)] public void Stage_shape_inlet_matches_outlet() { - var stage = new QuicConnectionStage( - ActorRefs.Nobody, - new TurboClientOptions()); + var stage = new QuicConnectionStage(ActorRefs.Nobody); var shape = stage.Shape; - Assert.Same(shape, stage.Shape); // Shape should be consistent + Assert.Same(shape, stage.Shape); } [Fact(Timeout = 5000)] - public void Stage_with_custom_client_options_should_work() + public void Stage_with_connection_migration_default_should_work() { - var clientOptions = new TurboClientOptions - { - ConnectTimeout = TimeSpan.FromSeconds(30) - }; - - var stage = new QuicConnectionStage( - ActorRefs.Nobody, - clientOptions, - allowConnectionMigration: true); + var stage = new QuicConnectionStage(ActorRefs.Nobody); Assert.NotNull(stage); } @@ -100,15 +85,38 @@ public void Multiple_stages_should_be_independent() { var stage1 = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: true); var stage2 = new QuicConnectionStage( ActorRefs.Nobody, - new TurboClientOptions(), allowConnectionMigration: false); Assert.NotSame(stage1, stage2); Assert.NotSame(stage1.Shape, stage2.Shape); } + + [Fact(Timeout = 5000)] + public void Stage_inlet_should_be_named_correctly() + { + var stage = new QuicConnectionStage(ActorRefs.Nobody); + + Assert.Equal("QuicConnection.In", stage.Shape.Inlet.Name); + } + + [Fact(Timeout = 5000)] + public void Stage_outlet_should_be_named_correctly() + { + var stage = new QuicConnectionStage(ActorRefs.Nobody); + + Assert.Equal("QuicConnection.Out", stage.Shape.Outlet.Name); + } + + [Fact(Timeout = 5000)] + public void Stage_should_have_single_inlet_and_outlet() + { + var stage = new QuicConnectionStage(ActorRefs.Nobody); + + Assert.Single(stage.Shape.Inlets); + Assert.Single(stage.Shape.Outlets); + } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Transport/QuicOptionsSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicOptionsSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/QuicOptionsSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicOptionsSpec.cs index af1ac18bc..c6a0b7e55 100644 --- a/src/TurboHTTP.Tests/Transport/QuicOptionsSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicOptionsSpec.cs @@ -1,10 +1,9 @@ using System.Net.Security; -using System.Runtime.Versioning; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Quic; #pragma warning disable CA1416 -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Quic; #pragma warning disable CA1416 diff --git a/src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerErrorSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerErrorSpec.cs new file mode 100644 index 000000000..b67b754ba --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerErrorSpec.cs @@ -0,0 +1,146 @@ +using System.Net; +using System.Threading.Channels; +using Akka.Actor; +using Akka.TestKit.Xunit; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; + +#pragma warning disable CA1416 + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class QuicPumpManagerErrorSpec : TestKit +{ + private static readonly RequestEndpoint TestEndpoint = new() + { + Scheme = "https", + Host = "localhost", + Port = 443, + Version = HttpVersion.Version30 + }; + + private static (Channel inbound, ConnectionHandle handle) CreateTestHandle() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, TestEndpoint); + return (inbound, handle); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundComplete_ConnectionFailure_for_request_stream_on_AbruptClose() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // streamTypeValue < 0 → request stream; AbruptClose → InboundComplete(ConnectionFailure) + inbound.Writer.TryComplete(new AbruptCloseException()); + pump.StartInboundPump(handle, streamTypeValue: -1, TestEndpoint, connectionGen: 0, streamId: 42); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(QuicCloseKind.ConnectionFailure, msg.CloseKind); + Assert.Equal(42, msg.StreamId); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundComplete_ConnectionFailure_for_request_stream_on_wrapped_AbruptClose() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // ChannelClosedException wrapping AbruptCloseException → same outcome for request stream + inbound.Writer.TryComplete(new AbruptCloseException()); + pump.StartInboundPump(handle, streamTypeValue: -1, TestEndpoint, connectionGen: 3, streamId: 7); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(QuicCloseKind.ConnectionFailure, msg.CloseKind); + Assert.Equal(3, msg.Gen); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_not_send_InboundComplete_for_control_stream_on_AbruptClose() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // streamTypeValue >= 0 → control stream; AbruptClose closes silently with no InboundComplete + inbound.Writer.TryComplete(new AbruptCloseException()); + pump.StartInboundPump(handle, streamTypeValue: 0x00, TestEndpoint, connectionGen: 0, streamId: -2); + + await Task.Delay(150, TestContext.Current.CancellationToken); + await probe.ExpectNoMsgAsync(TimeSpan.Zero, TestContext.Current.CancellationToken); + + pump.StopAll(); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundPumpFailed_on_unexpected_exception() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // A non-AbruptClose exception → InboundPumpFailed(error, streamId) + inbound.Writer.TryComplete(new IOException("stream reset by peer")); + pump.StartInboundPump(handle, streamTypeValue: -1, TestEndpoint, connectionGen: 0, streamId: 99); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(msg.Error); + Assert.Equal(99, msg.StreamId); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_exit_silently_on_cancellation() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + var (_, handle) = CreateTestHandle(); + + pump.StartInboundPump(handle, streamTypeValue: -1, TestEndpoint, connectionGen: 0, streamId: 1); + pump.StopAll(); + + await Task.Delay(150, TestContext.Current.CancellationToken); + await probe.ExpectNoMsgAsync(TimeSpan.Zero, TestContext.Current.CancellationToken); + } + + [Fact(Timeout = 5000)] + public async Task AcceptLoop_should_exit_silently_on_cancellation() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + + var provider = new FakeClientProvider(); // blocks AcceptInboundStreamAsync until cancelled + var options = new QuicOptions { Host = "localhost", Port = 443 }; + var connHandle = new QuicConnectionHandle(provider, options, TestEndpoint); + + pump.StartInboundAcceptLoop(connHandle); + pump.StopAll(); + + await Task.Delay(150, TestContext.Current.CancellationToken); + await probe.ExpectNoMsgAsync(TimeSpan.Zero, TestContext.Current.CancellationToken); + } + + [Fact(Timeout = 5000)] + public async Task AcceptLoop_should_send_InboundStreamReady_when_stream_accepted() + { + var probe = CreateTestProbe(); + var pump = new QuicPumpManager(probe.Ref); + + // inboundBytes[0] = stream-type varint (0x00 = control stream) + var provider = new FakeClientProvider(inboundBytes: [0x00]); + var options = new QuicOptions { Host = "localhost", Port = 443 }; + var connHandle = new QuicConnectionHandle(provider, options, TestEndpoint); + + pump.StartInboundAcceptLoop(connHandle); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(msg.Stream); + Assert.Equal(0x00, msg.Stream.StreamTypeValue); + + pump.StopAll(); + } +} diff --git a/src/TurboHTTP.StreamTests/Transport/QuicPumpManagerSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerSpec.cs similarity index 71% rename from src/TurboHTTP.StreamTests/Transport/QuicPumpManagerSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerSpec.cs index c3e197901..c410f815c 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicPumpManagerSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicPumpManagerSpec.cs @@ -1,11 +1,10 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicPumpManagerSpec { @@ -31,7 +30,7 @@ public void StartInboundPump_should_not_throw() var handle = CreateTestHandle(); // Should complete without throwing - pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1); + pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: 1); pumpMgr.StopAll(); } @@ -53,8 +52,8 @@ public void StopAll_should_cancel_pumps() var handle1 = CreateTestHandle(); var handle2 = CreateTestHandle(); - pumpMgr.StartInboundPump(handle1, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1); - pumpMgr.StartInboundPump(handle2, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 2); + pumpMgr.StartInboundPump(handle1, -1, TestEndpoint, connectionGen: 0, streamId: 1); + pumpMgr.StartInboundPump(handle2, -1, TestEndpoint, connectionGen: 0, streamId: 2); // Stop all should complete without throwing pumpMgr.StopAll(); @@ -71,7 +70,7 @@ public void Multiple_pumps_can_be_started() for (var i = 0; i < 5; i++) { var handle = CreateTestHandle(); - pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: i); + pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: i); } // StopAll should handle all pumps @@ -84,7 +83,7 @@ public void Control_stream_pump_should_not_throw() var pumpMgr = new QuicPumpManager(ActorRefs.Nobody); var handle = CreateTestHandle(); - pumpMgr.StartInboundPump(handle, Http3StreamType.Control, TestEndpoint, connectionGen: 0); + pumpMgr.StartInboundPump(handle, 0x00, TestEndpoint, connectionGen: 0, streamId: -2); pumpMgr.StopAll(); } @@ -95,19 +94,18 @@ public void Encoder_stream_pump_should_not_throw() var pumpMgr = new QuicPumpManager(ActorRefs.Nobody); var handle = CreateTestHandle(); - pumpMgr.StartInboundPump(handle, Http3StreamType.QpackEncoder, TestEndpoint, connectionGen: 0); + pumpMgr.StartInboundPump(handle, 0x02, TestEndpoint, connectionGen: 0, streamId: -3); pumpMgr.StopAll(); } [Fact(Timeout = 5000)] - public void StartInboundPump_without_stream_id_should_work() + public void StartInboundPump_with_explicit_stream_id_should_work() { var pumpMgr = new QuicPumpManager(ActorRefs.Nobody); var handle = CreateTestHandle(); - // Default streamId = -1 for connection-level streams - pumpMgr.StartInboundPump(handle, Http3StreamType.Control, TestEndpoint, connectionGen: 0); + pumpMgr.StartInboundPump(handle, 0x00, TestEndpoint, connectionGen: 0, streamId: -2); pumpMgr.StopAll(); } @@ -118,7 +116,7 @@ public void StopAll_can_be_called_multiple_times() var pumpMgr = new QuicPumpManager(ActorRefs.Nobody); var handle = CreateTestHandle(); - pumpMgr.StartInboundPump(handle, Http3StreamType.Request, TestEndpoint, connectionGen: 0, streamId: 1); + pumpMgr.StartInboundPump(handle, -1, TestEndpoint, connectionGen: 0, streamId: 1); pumpMgr.StopAll(); pumpMgr.StopAll(); @@ -127,4 +125,4 @@ public void StopAll_can_be_called_multiple_times() // Should not throw Assert.True(true); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.StreamTests/Transport/QuicStreamRouterEnhancedSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterEnhancedSpec.cs similarity index 90% rename from src/TurboHTTP.StreamTests/Transport/QuicStreamRouterEnhancedSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterEnhancedSpec.cs index c090286b1..433480110 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicStreamRouterEnhancedSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterEnhancedSpec.cs @@ -1,12 +1,11 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicStreamRouterEnhancedSpec { @@ -38,15 +37,16 @@ private static (ConnectionHandle Handle, ChannelReader OutboundRe public void RouteTaggedItem_should_route_encoder_to_pending_when_no_handle() { var (router, ops) = CreateRouter(); - var pendingEncoder = new Queue(); - var encoderData = Http3NetworkBuffer.Rent(4); - encoderData.StreamType = Http3StreamType.QpackEncoder; + var encoderData = RoutedNetworkBuffer.Rent(4); + encoderData.StreamTypeValue = 0x02; encoderData.Length = 3; - router.RouteTaggedItem(encoderData, null, new Queue(), null, pendingEncoder); + var encoderState = new TypedStreamState { StreamId = -3 }; + var typedStreams = new Dictionary { [0x02] = encoderState }; + router.RouteTaggedItem(encoderData, 0x02, typedStreams); - Assert.Single(pendingEncoder); + Assert.Single(encoderState.PendingItems); Assert.True(ops.PullInputCount > 0); } @@ -56,12 +56,13 @@ public void RouteTaggedItem_should_write_encoder_to_handle_when_available() var (router, _) = CreateRouter(); var (encoderHandle, encoderReader) = CreateTestHandle(); - var encoderData = Http3NetworkBuffer.Rent(4); - encoderData.StreamType = Http3StreamType.QpackEncoder; + var encoderData = RoutedNetworkBuffer.Rent(4); + encoderData.StreamTypeValue = 0x02; encoderData.Length = 3; - router.RouteTaggedItem(encoderData, null, new Queue(), encoderHandle, - new Queue()); + var encoderState = new TypedStreamState { Handle = encoderHandle, StreamId = -3 }; + var typedStreams = new Dictionary { [0x02] = encoderState }; + router.RouteTaggedItem(encoderData, 0x02, typedStreams); Assert.True(encoderReader.TryRead(out _)); } @@ -238,7 +239,7 @@ public void EnsureStreamContext_should_reject_null_scheme() { var (router, _) = CreateRouter(); var endpoint = new RequestEndpoint - { Scheme = null!, Host = "localhost", Port = 443, Version = HttpVersion.Version30 }; + { Scheme = null!, Host = "localhost", Port = 443, Version = HttpVersion.Version30 }; var item = new ConnectItem(new QuicOptions { Host = "localhost", Port = 443 }) { Key = endpoint @@ -298,13 +299,12 @@ public void RouteTaggedItem_request_with_wrong_stream_id_should_handle_gracefull var ctx = router.GetOrCreateContext(1); ctx.Handle = handle; - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Request; + var dataItem = RoutedNetworkBuffer.Rent(4); dataItem.StreamId = 999; // Different from expected dataItem.Length = 3; - // Should not throw - routing handles mismatched stream IDs gracefully - router.RouteTaggedItem(dataItem, null, new Queue(), null, new Queue()); + var typedStreams = new Dictionary(); + router.RouteTaggedItem(dataItem, -1, typedStreams); // Verify the operation completed without error Assert.NotNull(router); diff --git a/src/TurboHTTP.StreamTests/Transport/QuicStreamRouterSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterSpec.cs similarity index 91% rename from src/TurboHTTP.StreamTests/Transport/QuicStreamRouterSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterSpec.cs index 239fb6ce9..54f5decfa 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicStreamRouterSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicStreamRouterSpec.cs @@ -1,12 +1,11 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicStreamRouterSpec { @@ -101,12 +100,12 @@ public void RouteTaggedItem_should_write_to_handle_for_known_request_stream() var ctx = router.GetOrCreateContext(1); ctx.Handle = handle; - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Request; + var dataItem = RoutedNetworkBuffer.Rent(4); dataItem.StreamId = 1; dataItem.Length = 3; - router.RouteTaggedItem(dataItem, null, new Queue(), null, new Queue()); + var typedStreams = new Dictionary(); + router.RouteTaggedItem(dataItem, -1, typedStreams); Assert.True(outboundReader.TryRead(out _)); } @@ -117,12 +116,12 @@ public void RouteTaggedItem_should_enqueue_when_handle_not_ready() var (router, ops) = CreateRouter(); router.GetOrCreateContext(1); - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Request; + var dataItem = RoutedNetworkBuffer.Rent(4); dataItem.StreamId = 1; dataItem.Length = 3; - router.RouteTaggedItem(dataItem, null, new Queue(), null, new Queue()); + var typedStreams = new Dictionary(); + router.RouteTaggedItem(dataItem, -1, typedStreams); Assert.Single(router.RequestStreams[1].PendingWrites); Assert.True(ops.PullInputCount > 0); @@ -132,15 +131,16 @@ public void RouteTaggedItem_should_enqueue_when_handle_not_ready() public void RouteTaggedItem_should_route_control_to_pending_queue_when_no_handle() { var (router, ops) = CreateRouter(); - var pendingControl = new Queue(); + var controlState = new TypedStreamState { StreamId = -2 }; + var typedStreams = new Dictionary { [0x00] = controlState }; - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Control; + var dataItem = RoutedNetworkBuffer.Rent(4); + dataItem.StreamTypeValue = 0x00; dataItem.Length = 3; - router.RouteTaggedItem(dataItem, null, pendingControl, null, new Queue()); + router.RouteTaggedItem(dataItem, 0x00, typedStreams); - Assert.Single(pendingControl); + Assert.Single(controlState.PendingItems); Assert.True(ops.PullInputCount > 0); } @@ -149,12 +149,14 @@ public void RouteTaggedItem_should_write_control_to_handle_when_available() { var (router, _) = CreateRouter(); var (controlHandle, controlReader) = CreateTestHandle(); + var controlState = new TypedStreamState { Handle = controlHandle, StreamId = -2 }; + var typedStreams = new Dictionary { [0x00] = controlState }; - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Control; + var dataItem = RoutedNetworkBuffer.Rent(4); + dataItem.StreamTypeValue = 0x00; dataItem.Length = 3; - router.RouteTaggedItem(dataItem, controlHandle, new Queue(), null, new Queue()); + router.RouteTaggedItem(dataItem, 0x00, typedStreams); Assert.True(controlReader.TryRead(out _)); } diff --git a/src/Servus.Akka.Tests/IO/Quic/QuicTransportEventSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicTransportEventSpec.cs new file mode 100644 index 000000000..ba309ba1f --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/QuicTransportEventSpec.cs @@ -0,0 +1,148 @@ +using System.Net; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; + +#pragma warning disable CA1416 + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class QuicTransportEventSpec +{ + [Fact(Timeout = 5000)] + public void RequestLeaseAcquired_should_preserve_fields() + { + var lease = CreateTestConnectionLease(); + var evt = new RequestLeaseAcquired(lease, 42); + + Assert.Same(lease, evt.Lease); + Assert.Equal(42, evt.StreamId); + } + + [Fact(Timeout = 5000)] + public void TypedLeaseAcquired_should_preserve_fields() + { + var lease = CreateTestConnectionLease(); + var evt = new TypedLeaseAcquired(lease, 0x00, 7); + + Assert.Same(lease, evt.Lease); + Assert.Equal(0x00, evt.StreamTypeValue); + Assert.Equal(7, evt.StreamId); + } + + [Fact(Timeout = 5000)] + public void AcquisitionFailed_should_preserve_error() + { + var ex = new IOException("test"); + var evt = new Servus.Akka.IO.Quic.AcquisitionFailed(ex); + + Assert.Same(ex, evt.Error); + } + + [Fact(Timeout = 5000)] + public void InboundData_should_preserve_fields() + { + var buf = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + var evt = new Servus.Akka.IO.Quic.InboundData(buf, 5); + + Assert.Same(buf, evt.Item); + Assert.Equal(5, evt.Gen); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void InboundComplete_should_preserve_fields() + { + var evt = new Servus.Akka.IO.Quic.InboundComplete(QuicCloseKind.ConnectionFailure, 3, 42); + + Assert.Equal(QuicCloseKind.ConnectionFailure, evt.CloseKind); + Assert.Equal(3, evt.Gen); + Assert.Equal(42, evt.StreamId); + } + + [Fact(Timeout = 5000)] + public void InboundPumpFailed_should_preserve_fields() + { + var ex = new IOException("pump failed"); + var evt = new Servus.Akka.IO.Quic.InboundPumpFailed(ex, 99); + + Assert.Same(ex, evt.Error); + Assert.Equal(99, evt.StreamId); + } + + [Fact(Timeout = 5000)] + public void OutboundWriteDone_should_implement_interface() + { + IQuicTransportEvent evt = new Servus.Akka.IO.Quic.OutboundWriteDone(); + + Assert.IsType(evt); + } + + [Fact(Timeout = 5000)] + public void OutboundWriteFailed_should_preserve_error() + { + var ex = new IOException("write failed"); + var evt = new Servus.Akka.IO.Quic.OutboundWriteFailed(ex); + + Assert.Same(ex, evt.Error); + } + + [Fact(Timeout = 5000)] + public void EarlyDataRejected_should_preserve_buffer() + { + var buf = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + var evt = new EarlyDataRejected(buf); + + Assert.Same(buf, evt.Buffer); + + buf.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ConnectionMigrated_should_preserve_endpoints() + { + var oldEp = new IPEndPoint(IPAddress.Loopback, 1234); + var newEp = new IPEndPoint(IPAddress.Loopback, 5678); + var evt = new ConnectionMigrated(oldEp, newEp); + + Assert.Equal(oldEp, evt.OldLocalEndPoint); + Assert.Equal(newEp, evt.NewLocalEndPoint); + } + + [Fact(Timeout = 5000)] + public void ConnectionMigrated_should_allow_null_endpoints() + { + var evt = new ConnectionMigrated(null, null); + + Assert.Null(evt.OldLocalEndPoint); + Assert.Null(evt.NewLocalEndPoint); + } + + [Fact(Timeout = 5000)] + public void InboundComplete_equality_should_compare_all_fields() + { + var a = new Servus.Akka.IO.Quic.InboundComplete(QuicCloseKind.RequestStreamComplete, 1, 42); + var b = new Servus.Akka.IO.Quic.InboundComplete(QuicCloseKind.RequestStreamComplete, 1, 42); + var c = new Servus.Akka.IO.Quic.InboundComplete(QuicCloseKind.ConnectionFailure, 1, 42); + + Assert.Equal(a, b); + Assert.NotEqual(a, c); + } + + private static ConnectionLease CreateTestConnectionLease() + { + var inbound = System.Threading.Channels.Channel.CreateUnbounded(); + var outbound = System.Threading.Channels.Channel.CreateUnbounded(); + var key = new RequestEndpoint + { + Scheme = "https", + Host = "localhost", + Port = 443, + Version = new Version(3, 0) + }; + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, key); + var state = new ClientState(Stream.Null, inbound, outbound); + return new ConnectionLease(handle, state); + } +} diff --git a/src/Servus.Akka.Tests/IO/Quic/QuicTransportFactorySpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicTransportFactorySpec.cs new file mode 100644 index 000000000..6c7fb9c6e --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/QuicTransportFactorySpec.cs @@ -0,0 +1,58 @@ +using Akka.Actor; +using Servus.Akka.IO.Quic; + +#pragma warning disable CA1416 + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class QuicTransportFactorySpec +{ + [Fact(Timeout = 5000)] + public void QuicTransportFactory_should_accept_valid_actor_ref() + { + var factory = new QuicTransportFactory(ActorRefs.Nobody); + + Assert.NotNull(factory); + } + + [Fact(Timeout = 5000)] + public void Create_should_return_non_null_flow() + { + var factory = new QuicTransportFactory(ActorRefs.Nobody); + + var flow = factory.Create(); + + Assert.NotNull(flow); + } + + [Fact(Timeout = 5000)] + public void Create_should_return_independent_flows() + { + var factory = new QuicTransportFactory(ActorRefs.Nobody); + + var flow1 = factory.Create(); + var flow2 = factory.Create(); + + Assert.NotSame(flow1, flow2); + } + + [Fact(Timeout = 5000)] + public void QuicTransportFactory_should_default_allow_connection_migration_to_true() + { + var factory = new QuicTransportFactory(ActorRefs.Nobody); + + var flow = factory.Create(); + + Assert.NotNull(flow); + } + + [Fact(Timeout = 5000)] + public void QuicTransportFactory_should_accept_migration_disabled() + { + var factory = new QuicTransportFactory(ActorRefs.Nobody, allowConnectionMigration: false); + + var flow = factory.Create(); + + Assert.NotNull(flow); + } +} diff --git a/src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineLifecycleSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineLifecycleSpec.cs similarity index 86% rename from src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineLifecycleSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineLifecycleSpec.cs index 24c9b1b2e..246ee9476 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineLifecycleSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineLifecycleSpec.cs @@ -2,14 +2,12 @@ using System.Threading.Channels; using Akka.Actor; using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; -using TurboHTTP.Transport.Tcp; -using Quic = TurboHTTP.Transport.Quic; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; #pragma warning disable CA1416 @@ -49,12 +47,12 @@ private static (QuicTransportStateMachine Sm, MockTransportOperations Ops) Creat bool allowConnectionMigration = true) { var ops = new MockTransportOperations(); - var sm = new QuicTransportStateMachine( - ops, - ActorRefs.Nobody, - ActorRefs.Nobody, - new TurboClientOptions(), - allowConnectionMigration); + var sm = new QuicTransportStateMachine(ops, ActorRefs.Nobody, ActorRefs.Nobody, allowConnectionMigration); + sm.HandlePush(new OpenTypedStreamItem(0x00, -2, Outbound: true)); + sm.HandlePush(new OpenTypedStreamItem(0x02, -3, Outbound: true)); + sm.HandlePush(new OpenTypedStreamItem(0x03, -4, Outbound: false)); + sm.HandlePush(new ProtocolReadyItem()); + ops.PullInputCount = 0; return (sm, ops); } @@ -130,9 +128,8 @@ public void TypedLeaseAcquired_Control_should_flush_pending_and_open_encoder() { var (sm, ops) = CreateStateMachine(); - // Push control data before control stream is ready - var controlData = Http3NetworkBuffer.Rent(4); - controlData.StreamType = Http3StreamType.Control; + var controlData = RoutedNetworkBuffer.Rent(4); + controlData.StreamTypeValue = 0x00; controlData.Length = 3; controlData.Key = TestEndpoint; sm.HandlePush(controlData); @@ -140,7 +137,7 @@ public void TypedLeaseAcquired_Control_should_flush_pending_and_open_encoder() var lease = CreateTestLease(); ops.PullInputCount = 0; - sm.Dispatch(new TypedLeaseAcquired(lease, Http3StreamType.Control)); + sm.Dispatch(new TypedLeaseAcquired(lease, 0x00, -2)); Assert.True(ops.PullInputCount > 0); } @@ -151,7 +148,7 @@ public void TypedLeaseAcquired_QpackEncoder_should_flush_pending() var (sm, ops) = CreateStateMachine(); var lease = CreateTestLease(); - sm.Dispatch(new TypedLeaseAcquired(lease, Http3StreamType.QpackEncoder)); + sm.Dispatch(new TypedLeaseAcquired(lease, 0x02, -3)); Assert.True(ops.PullInputCount > 0); } @@ -211,9 +208,10 @@ public void EarlyDataRejected_should_requeue_to_first_stream() { var (sm, ops) = CreateStateMachine(); + sm.HandlePush(new ConnectItem(TestQuicOptions) { Key = TestEndpoint }); + // Create a pending request stream - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Request; + var dataItem = RoutedNetworkBuffer.Rent(4); dataItem.StreamId = 1; dataItem.Length = 3; dataItem.Key = TestEndpoint; @@ -232,14 +230,14 @@ public void Multiple_streams_should_be_routed_independently() { var (sm, ops) = CreateStateMachine(); - var stream1 = Http3NetworkBuffer.Rent(4); - stream1.StreamType = Http3StreamType.Request; + sm.HandlePush(new ConnectItem(TestQuicOptions) { Key = TestEndpoint }); + + var stream1 = RoutedNetworkBuffer.Rent(4); stream1.StreamId = 1; stream1.Length = 3; stream1.Key = TestEndpoint; - var stream3 = Http3NetworkBuffer.Rent(4); - stream3.StreamType = Http3StreamType.Request; + var stream3 = RoutedNetworkBuffer.Rent(4); stream3.StreamId = 3; stream3.Length = 3; stream3.Key = TestEndpoint; @@ -255,9 +253,10 @@ public void Untagged_buffer_should_route_to_first_stream_with_handle() { var (sm, ops) = CreateStateMachine(); + sm.HandlePush(new ConnectItem(TestQuicOptions) { Key = TestEndpoint }); + // Create a request stream context - var requestData = Http3NetworkBuffer.Rent(4); - requestData.StreamType = Http3StreamType.Request; + var requestData = RoutedNetworkBuffer.Rent(4); requestData.StreamId = 1; requestData.Length = 3; requestData.Key = TestEndpoint; @@ -278,7 +277,7 @@ public void AcquisitionFailed_without_pending_connect_should_noop() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.AcquisitionFailed(new Exception("failed"))); + sm.Dispatch(new Servus.Akka.IO.Quic.AcquisitionFailed(new Exception("failed"))); // No outputs pushed since no pending connect Assert.Empty(ops.PushedOutputs); @@ -289,7 +288,7 @@ public void Inbound_pump_failure_should_trigger_reconnect() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.InboundPumpFailed(new IOException("pump failed"), 1)); + sm.Dispatch(new Servus.Akka.IO.Quic.InboundPumpFailed(new IOException("pump failed"), 1)); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.ConnectionFailure }); @@ -316,7 +315,7 @@ public void Outbound_write_failure_should_trigger_close() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.OutboundWriteFailed(new IOException("write error"))); + sm.Dispatch(new Servus.Akka.IO.Quic.OutboundWriteFailed(new IOException("write error"))); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.WriteFailed }); diff --git a/src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineSpec.cs b/src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineSpec.cs similarity index 85% rename from src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineSpec.cs rename to src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineSpec.cs index 99c85a680..25a7ba968 100644 --- a/src/TurboHTTP.StreamTests/Transport/QuicTransportStateMachineSpec.cs +++ b/src/Servus.Akka.Tests/IO/Quic/QuicTransportStateMachineSpec.cs @@ -1,15 +1,10 @@ using System.Net; using Akka.Actor; -using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Protocol.Http11; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; -using Quic = TurboHTTP.Transport.Quic; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Quic; public sealed class QuicTransportStateMachineSpec { @@ -31,12 +26,12 @@ private static (QuicTransportStateMachine Sm, MockTransportOperations Ops) Creat bool allowConnectionMigration = true) { var ops = new MockTransportOperations(); - var sm = new QuicTransportStateMachine( - ops, - ActorRefs.Nobody, - ActorRefs.Nobody, - new TurboClientOptions(), - allowConnectionMigration); + var sm = new QuicTransportStateMachine(ops, ActorRefs.Nobody, ActorRefs.Nobody, allowConnectionMigration); + sm.HandlePush(new OpenTypedStreamItem(0x00, -2, Outbound: true)); + sm.HandlePush(new OpenTypedStreamItem(0x02, -3, Outbound: true)); + sm.HandlePush(new OpenTypedStreamItem(0x03, -4, Outbound: false)); + sm.HandlePush(new ProtocolReadyItem()); + ops.PullInputCount = 0; return (sm, ops); } @@ -70,7 +65,7 @@ public void Dispatch_OutboundWriteDone_should_signal_pull_input() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.OutboundWriteDone()); + sm.Dispatch(new OutboundWriteDone()); Assert.Equal(1, ops.PullInputCount); } @@ -80,7 +75,7 @@ public void Dispatch_OutboundWriteFailed_should_push_quic_close_item() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.OutboundWriteFailed(new IOException("write failed"))); + sm.Dispatch(new OutboundWriteFailed(new IOException("write failed"))); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.WriteFailed }); } @@ -94,7 +89,7 @@ public void Dispatch_AcquisitionFailed_should_cancel_connect_timer() sm.HandlePush(connectItem); ops.CancelledTimers.Clear(); - sm.Dispatch(new Quic.AcquisitionFailed(new Exception("failed"))); + sm.Dispatch(new AcquisitionFailed(new Exception("failed"))); Assert.Contains("connect-timeout", ops.CancelledTimers); } @@ -109,7 +104,7 @@ public void Dispatch_AcquisitionFailed_should_push_close_and_pull() ops.PushedOutputs.Clear(); ops.PullInputCount = 0; - sm.Dispatch(new Quic.AcquisitionFailed(new Exception("failed"))); + sm.Dispatch(new AcquisitionFailed(new Exception("failed"))); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.AcquisitionFailed }); Assert.True(ops.PullInputCount > 0); @@ -120,7 +115,7 @@ public void Dispatch_InboundComplete_clean_should_push_request_stream_complete() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.InboundComplete(TlsCloseKind.CleanClose, 0, StreamId: 1)); + sm.Dispatch(new InboundComplete(QuicCloseKind.RequestStreamComplete, 0, StreamId: 1)); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.RequestStreamComplete }); @@ -131,7 +126,7 @@ public void Dispatch_InboundComplete_abrupt_should_push_connection_failure() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.InboundComplete(TlsCloseKind.AbruptClose, 0, StreamId: 1)); + sm.Dispatch(new InboundComplete(QuicCloseKind.ConnectionFailure, 0, StreamId: 1)); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.ConnectionFailure }); @@ -142,7 +137,7 @@ public void Dispatch_InboundPumpFailed_should_treat_as_abrupt_close() { var (sm, ops) = CreateStateMachine(); - sm.Dispatch(new Quic.InboundPumpFailed(new IOException("pump failed"), StreamId: 1)); + sm.Dispatch(new InboundPumpFailed(new IOException("pump failed"), StreamId: 1)); Assert.Contains(ops.PushedOutputs, item => item is QuicCloseItem { Kind: QuicCloseKind.ConnectionFailure }); @@ -189,8 +184,9 @@ public void HandlePush_tagged_buffer_should_signal_pull_when_no_connection() { var (sm, ops) = CreateStateMachine(); - var dataItem = Http3NetworkBuffer.Rent(4); - dataItem.StreamType = Http3StreamType.Request; + sm.HandlePush(new ConnectItem(TestQuicOptions) { Key = TestEndpoint }); + + var dataItem = RoutedNetworkBuffer.Rent(4); dataItem.StreamId = 1; dataItem.Length = 3; dataItem.Key = TestEndpoint; @@ -218,6 +214,8 @@ public void HandlePush_EndOfRequest_should_signal_pull() { var (sm, ops) = CreateStateMachine(); + sm.HandlePush(new ConnectItem(TestQuicOptions) { Key = TestEndpoint }); + sm.HandlePush(new Http3EndOfRequestItem { Key = TestEndpoint, StreamId = 1 }); Assert.True(ops.PullInputCount > 0); @@ -228,7 +226,7 @@ public void HandlePush_ConnectionReuseItem_should_signal_pull() { var (sm, ops) = CreateStateMachine(); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("reuse")) { Key = TestEndpoint }); + sm.HandlePush(new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.Equal(1, ops.PullInputCount); } diff --git a/src/Servus.Akka.Tests/IO/Quic/StreamDirectionSpec.cs b/src/Servus.Akka.Tests/IO/Quic/StreamDirectionSpec.cs new file mode 100644 index 000000000..ecc11278c --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/StreamDirectionSpec.cs @@ -0,0 +1,40 @@ +using Servus.Akka.IO.Quic; + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class StreamDirectionSpec +{ + [Fact(Timeout = 5000)] + public void StreamDirection_should_have_bidirectional_value() + { + Assert.Equal(0, (int)StreamDirection.Bidirectional); + } + + [Fact(Timeout = 5000)] + public void StreamDirection_should_have_write_only_value() + { + Assert.Equal(1, (int)StreamDirection.WriteOnly); + } + + [Fact(Timeout = 5000)] + public void StreamDirection_should_have_read_only_value() + { + Assert.Equal(2, (int)StreamDirection.ReadOnly); + } + + [Fact(Timeout = 5000)] + public void StreamDirection_should_have_exactly_three_values() + { + var values = Enum.GetValues(); + + Assert.Equal(3, values.Length); + } + + [Fact(Timeout = 5000)] + public void StreamDirection_default_should_be_bidirectional() + { + var direction = default(StreamDirection); + + Assert.Equal(StreamDirection.Bidirectional, direction); + } +} diff --git a/src/Servus.Akka.Tests/IO/Quic/TypedStreamStateSpec.cs b/src/Servus.Akka.Tests/IO/Quic/TypedStreamStateSpec.cs new file mode 100644 index 000000000..e1e81c4ae --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Quic/TypedStreamStateSpec.cs @@ -0,0 +1,83 @@ +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.Tests.Utils; + +namespace Servus.Akka.Tests.IO.Quic; + +public sealed class TypedStreamStateSpec +{ + [Fact(Timeout = 5000)] + public void TypedStreamState_should_have_null_handle_by_default() + { + var state = new TypedStreamState(); + + Assert.Null(state.Handle); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_should_have_empty_pending_items_by_default() + { + var state = new TypedStreamState(); + + Assert.Empty(state.PendingItems); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_should_have_zero_stream_id_by_default() + { + var state = new TypedStreamState(); + + Assert.Equal(0, state.StreamId); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_should_have_zero_original_synthetic_stream_id_by_default() + { + var state = new TypedStreamState(); + + Assert.Equal(0, state.OriginalSyntheticStreamId); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_should_have_false_is_outbound_by_default() + { + var state = new TypedStreamState(); + + Assert.False(state.IsOutbound); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_should_allow_setting_all_fields() + { + var state = new TypedStreamState + { + StreamId = 42, + OriginalSyntheticStreamId = -2, + IsOutbound = true + }; + + Assert.Equal(42, state.StreamId); + Assert.Equal(-2, state.OriginalSyntheticStreamId); + Assert.True(state.IsOutbound); + } + + [Fact(Timeout = 5000)] + public void TypedStreamState_PendingItems_should_support_enqueue_dequeue() + { + var state = new TypedStreamState(); + + var buf1 = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + var buf2 = NetworkBufferTestExtensions.FromArray([4, 5, 6]); + + state.PendingItems.Enqueue(buf1); + state.PendingItems.Enqueue(buf2); + + Assert.Equal(2, state.PendingItems.Count); + + var first = state.PendingItems.Dequeue(); + Assert.Same(buf1, first); + + first.Dispose(); + state.PendingItems.Dequeue().Dispose(); + } +} diff --git a/src/Servus.Akka.Tests/IO/RequestEndpointSpec.cs b/src/Servus.Akka.Tests/IO/RequestEndpointSpec.cs new file mode 100644 index 000000000..ad35b3cae --- /dev/null +++ b/src/Servus.Akka.Tests/IO/RequestEndpointSpec.cs @@ -0,0 +1,196 @@ +using System.Net; +using Servus.Akka.IO; + +namespace Servus.Akka.Tests.IO; + +public sealed class RequestEndpointSpec +{ + private static readonly RequestEndpoint TestEndpoint = new() + { + Scheme = "https", + Host = "example.com", + Port = 443, + Version = HttpVersion.Version20 + }; + + [Fact(Timeout = 5000)] + public void FromRequest_should_extract_host_port_scheme_version() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com:8443/path") + { + Version = HttpVersion.Version20 + }; + + var endpoint = RequestEndpoint.FromRequest(request); + + Assert.Equal("example.com", endpoint.Host); + Assert.Equal((ushort)8443, endpoint.Port); + Assert.Equal("https", endpoint.Scheme); + Assert.Equal(HttpVersion.Version20, endpoint.Version); + } + + [Fact(Timeout = 5000)] + public void FromRequest_should_use_default_https_port() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/path") + { + Version = HttpVersion.Version11 + }; + + var endpoint = RequestEndpoint.FromRequest(request); + + Assert.Equal((ushort)443, endpoint.Port); + } + + [Fact(Timeout = 5000)] + public void FromRequest_should_use_default_http_port() + { + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/path") + { + Version = HttpVersion.Version11 + }; + + var endpoint = RequestEndpoint.FromRequest(request); + + Assert.Equal((ushort)80, endpoint.Port); + } + + [Fact(Timeout = 5000)] + public void FromRequest_should_throw_on_null_request() + { + Assert.Throws(() => RequestEndpoint.FromRequest(null!)); + } + + [Fact(Timeout = 5000)] + public void FromRequest_should_throw_on_null_version() + { + Assert.Throws(() => + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/path") + { + Version = null! + }; + }); + } + + [Fact(Timeout = 5000)] + public void FromRequest_should_throw_on_null_request_uri() + { + var request = new HttpRequestMessage + { + Version = HttpVersion.Version11, + RequestUri = null + }; + + Assert.Throws(() => RequestEndpoint.FromRequest(request)); + } + + [Fact(Timeout = 5000)] + public void Default_should_return_empty_endpoint() + { + var def = RequestEndpoint.Default; + + Assert.Equal(string.Empty, def.Host); + Assert.Equal(string.Empty, def.Scheme); + Assert.Equal(ushort.MinValue, def.Port); + Assert.Equal(HttpVersion.Unknown, def.Version); + } + + [Fact(Timeout = 5000)] + public void Equals_should_be_case_insensitive_for_host() + { + var upper = TestEndpoint with { Host = "EXAMPLE.COM" }; + var lower = TestEndpoint with { Host = "example.com" }; + + Assert.Equal(upper, lower); + } + + [Fact(Timeout = 5000)] + public void Equals_should_be_case_insensitive_for_scheme() + { + var upper = TestEndpoint with { Scheme = "HTTPS" }; + var lower = TestEndpoint with { Scheme = "https" }; + + Assert.Equal(upper, lower); + } + + [Fact(Timeout = 5000)] + public void Equals_should_be_sensitive_for_port() + { + var port443 = TestEndpoint with { Port = 443 }; + var port8443 = TestEndpoint with { Port = 8443 }; + + Assert.NotEqual(port443, port8443); + } + + [Fact(Timeout = 5000)] + public void Equals_should_be_sensitive_for_version() + { + var http20 = TestEndpoint with { Version = HttpVersion.Version20 }; + var http11 = TestEndpoint with { Version = HttpVersion.Version11 }; + + Assert.NotEqual(http20, http11); + } + + [Fact(Timeout = 5000)] + public void Equals_should_match_identical_endpoints() + { + var a = TestEndpoint; + var b = TestEndpoint; + + Assert.Equal(a, b); + Assert.True(a == b); + } + + [Fact(Timeout = 5000)] + public void Equals_should_not_match_default_and_populated() + { + Assert.NotEqual(RequestEndpoint.Default, TestEndpoint); + } + + [Fact(Timeout = 5000)] + public void GetHashCode_should_be_consistent() + { + var hash1 = TestEndpoint.GetHashCode(); + var hash2 = TestEndpoint.GetHashCode(); + + Assert.Equal(hash1, hash2); + } + + [Fact(Timeout = 5000)] + public void GetHashCode_should_be_case_insensitive_for_host() + { + var upper = TestEndpoint with { Host = "EXAMPLE.COM" }; + var lower = TestEndpoint with { Host = "example.com" }; + + Assert.Equal(upper.GetHashCode(), lower.GetHashCode()); + } + + [Fact(Timeout = 5000)] + public void GetHashCode_should_be_case_insensitive_for_scheme() + { + var upper = TestEndpoint with { Scheme = "HTTPS" }; + var lower = TestEndpoint with { Scheme = "https" }; + + Assert.Equal(upper.GetHashCode(), lower.GetHashCode()); + } + + [Fact(Timeout = 5000)] + public void GetHashCode_should_differ_for_different_ports() + { + var port443 = TestEndpoint with { Port = 443 }; + var port8443 = TestEndpoint with { Port = 8443 }; + + Assert.NotEqual(port443.GetHashCode(), port8443.GetHashCode()); + } + + [Fact(Timeout = 5000)] + public void Inequality_operator_should_detect_different_endpoints() + { + var a = TestEndpoint; + var b = TestEndpoint with { Port = 8080 }; + + Assert.True(a != b); + Assert.False(a == b); + } +} diff --git a/src/TurboHTTP.Tests/Transport/TcpClientProviderSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpClientProviderSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/TcpClientProviderSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpClientProviderSpec.cs index ab67c09d7..41115e60c 100644 --- a/src/TurboHTTP.Tests/Transport/TcpClientProviderSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpClientProviderSpec.cs @@ -1,8 +1,9 @@ using System.Net; using System.Net.Sockets; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpClientProviderSpec { diff --git a/src/TurboHTTP.Tests/Transport/DirectConnectionFactorySpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionFactorySpec.cs similarity index 79% rename from src/TurboHTTP.Tests/Transport/DirectConnectionFactorySpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpConnectionFactorySpec.cs index 57b67cec9..2485a2868 100644 --- a/src/TurboHTTP.Tests/Transport/DirectConnectionFactorySpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionFactorySpec.cs @@ -1,14 +1,13 @@ using System.Net; using System.Net.Sockets; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; -public sealed class DirectConnectionFactorySpec : IAsyncLifetime +public sealed class TcpConnectionFactorySpec : IAsyncLifetime { private TcpListener? _listener; private int _port; @@ -48,7 +47,7 @@ public async Task EstablishAsync_should_return_live_lease() var endpoint = CreateEndpoint(_port); using var lease = - await DirectConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); Assert.NotNull(lease); Assert.True(lease.IsAlive); @@ -63,7 +62,7 @@ public async Task EstablishAsync_should_use_nobody_for_connection_actor() var endpoint = CreateEndpoint(_port); using var lease = - await DirectConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); Assert.Equal(ActorRefs.Nobody, lease.Handle.ConnectionActor); } @@ -77,7 +76,7 @@ public async Task EstablishAsync_should_send_outbound_data_to_server() var acceptTask = _listener!.AcceptTcpClientAsync(TestContext.Current.CancellationToken); using var lease = - await DirectConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); using var serverClient = await acceptTask; var serverStream = serverClient.GetStream(); @@ -101,19 +100,19 @@ public async Task EstablishAsync_should_set_max_concurrent_streams_to_version_de // HTTP/1.0 → 1 var endpoint10 = CreateEndpoint(_port, HttpVersion.Version10); using var lease10 = - await DirectConnectionFactory.EstablishAsync(options, endpoint10, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint10, TestContext.Current.CancellationToken); Assert.Equal(1, lease10.MaxConcurrentStreams); // HTTP/1.1 → 6 var endpoint11 = CreateEndpoint(_port, HttpVersion.Version11); using var lease11 = - await DirectConnectionFactory.EstablishAsync(options, endpoint11, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint11, TestContext.Current.CancellationToken); Assert.Equal(6, lease11.MaxConcurrentStreams); // HTTP/2 → 100 var endpoint20 = CreateEndpoint(_port, HttpVersion.Version20); using var lease20 = - await DirectConnectionFactory.EstablishAsync(options, endpoint20, TestContext.Current.CancellationToken); + await TcpConnectionFactory.EstablishAsync(options, endpoint20, TestContext.Current.CancellationToken); Assert.Equal(100, lease20.MaxConcurrentStreams); } @@ -126,7 +125,7 @@ public async Task EstablishAsync_should_throw_on_pre_cancelled_token() await cts.CancelAsync(); await Assert.ThrowsAnyAsync(() => - DirectConnectionFactory.EstablishAsync(options, endpoint, cts.Token)); + TcpConnectionFactory.EstablishAsync(options, endpoint, cts.Token)); } [Fact(Timeout = 5000)] @@ -144,7 +143,7 @@ public async Task EstablishAsync_should_throw_when_cancelled_during_connect() using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); await Assert.ThrowsAnyAsync(() => - DirectConnectionFactory.EstablishAsync(options, endpoint, cts.Token)); + TcpConnectionFactory.EstablishAsync(options, endpoint, cts.Token)); } [Fact(Timeout = 5000)] @@ -157,7 +156,7 @@ public async Task EstablishAsync_should_throw_on_connection_refused() var endpoint = CreateEndpoint(_port); await Assert.ThrowsAnyAsync(() => - DirectConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken)); + TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken)); } [Fact(Timeout = 5000)] @@ -166,7 +165,7 @@ public async Task Disposing_lease_should_cancel_its_token() var options = CreateOptions(); var endpoint = CreateEndpoint(_port); - var lease = await DirectConnectionFactory.EstablishAsync(options, endpoint, + var lease = await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); var token = lease.Token; @@ -183,7 +182,7 @@ public async Task Disposing_lease_should_mark_it_not_alive() var options = CreateOptions(); var endpoint = CreateEndpoint(_port); - var lease = await DirectConnectionFactory.EstablishAsync(options, endpoint, + var lease = await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); Assert.True(lease.IsAlive); @@ -201,7 +200,7 @@ public async Task Server_close_should_trigger_disposal() var acceptTask = _listener!.AcceptTcpClientAsync(TestContext.Current.CancellationToken); - var lease = await DirectConnectionFactory.EstablishAsync(options, endpoint, + var lease = await TcpConnectionFactory.EstablishAsync(options, endpoint, TestContext.Current.CancellationToken); using var serverClient = await acceptTask; @@ -225,6 +224,6 @@ public async Task EstablishAsync_should_throw_on_null_options() var endpoint = CreateEndpoint(80); await Assert.ThrowsAsync(() => - DirectConnectionFactory.EstablishAsync(null!, endpoint, TestContext.Current.CancellationToken)); + TcpConnectionFactory.EstablishAsync(null!, endpoint, TestContext.Current.CancellationToken)); } } \ No newline at end of file diff --git a/src/TurboHTTP.StreamTests/Transport/ConnectionManagerActorSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionManagerActorSpec.cs similarity index 74% rename from src/TurboHTTP.StreamTests/Transport/ConnectionManagerActorSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpConnectionManagerActorSpec.cs index b1e6b9d93..470661779 100644 --- a/src/TurboHTTP.StreamTests/Transport/ConnectionManagerActorSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionManagerActorSpec.cs @@ -1,12 +1,13 @@ using System.Net; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; +using Akka.TestKit.Xunit; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; -public sealed class TcpConnectionManagerActorSpec : StreamTestBase +public sealed class TcpConnectionManagerActorSpec : TestKit { private readonly InMemoryConnectionFactory _factory = new(); @@ -266,12 +267,18 @@ public async Task Multiple_hosts_should_maintain_separate_pools() var options1 = new TcpOptions { Host = "host1.example.com", Port = 80 }; var endpoint1 = new RequestEndpoint { - Host = "host1.example.com", Port = 80, Scheme = "http", Version = HttpVersion.Version11 + Host = "host1.example.com", + Port = 80, + Scheme = "http", + Version = HttpVersion.Version11 }; var options2 = new TcpOptions { Host = "host2.example.com", Port = 80 }; var endpoint2 = new RequestEndpoint { - Host = "host2.example.com", Port = 80, Scheme = "http", Version = HttpVersion.Version11 + Host = "host2.example.com", + Port = 80, + Scheme = "http", + Version = HttpVersion.Version11 }; var lease1 = @@ -394,6 +401,117 @@ await TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, lease2.Dispose(); } + [Fact(Timeout = 5000)] + public async Task Acquire_with_already_cancelled_token_should_be_ignored_by_actor() + { + var actor = CreateActor(); + var options = CreateOptions(); + var endpoint = CreateEndpoint(HttpVersion.Version11); + + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + // UnsafeRegister fires synchronously for already-cancelled tokens, so TCS is completed + // before actor.Tell. The actor receives a completed TCS and immediately returns. + await Assert.ThrowsAnyAsync(() => + TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, cts.Token)); + + // Actor must still be alive and functional + var lease = await TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + Assert.NotNull(lease); + + lease.Dispose(); + } + + [Fact(Timeout = 5000)] + public async Task Established_with_cancelled_caller_should_release_back_to_pool() + { + var slowFactory = new SlowConnectionFactory(TimeSpan.FromMilliseconds(200)); + var actor = Sys.ActorOf(Props.Create(() => + new TcpConnectionManagerActor(slowFactory, TimeSpan.FromSeconds(30), Timeout.InfiniteTimeSpan, + maxConnectionsPerServer: 1))); + var options = CreateOptions(); + var endpoint = CreateEndpoint(HttpVersion.Version11); + + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(30)); + + // acquire1: cancelled before factory completes; establishing slot held + var task1 = TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, cts.Token); + + // acquire2: queued because max-connections=1 slot is being established + var task2 = TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + + await Assert.ThrowsAnyAsync(() => task1); + + // When factory resolves, actor calls OnEstablished → TrySetResult(tcs1) fails → + // OnRelease → direct handoff to acquire2 + var lease = await task2; + Assert.NotNull(lease); + Assert.True(lease.IsAlive); + + lease.Dispose(); + } + + [Fact(Timeout = 5000)] + public async Task Acquire_should_skip_dead_idle_lease_and_establish_fresh_connection() + { + var actor = CreateActor(); + var options = CreateOptions(); + var endpoint = CreateEndpoint(HttpVersion.Version11); + + var lease1 = await TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + + // Release to idle queue + actor.Tell(new TcpConnectionManagerActor.Release(lease1, CanReuse: true)); + + // Wait for Release to be processed + await Task.Delay(50, TestContext.Current.CancellationToken); + + // Externally dispose the lease — IsAlive becomes false (stale idle) + lease1.Dispose(); + + // Acquire: scans idle, finds stale lease (IsAlive=false), disposes it, establishes fresh + var lease2 = await TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + Assert.NotSame(lease1, lease2); + Assert.True(lease2.IsAlive); + + lease2.Dispose(); + } + + [Fact(Timeout = 5000)] + public async Task EstablishFailed_should_cascade_to_pending_waiter() + { + var failOnce = new FailOnceConnectionFactory(); + var actor = Sys.ActorOf(Props.Create(() => + new TcpConnectionManagerActor(failOnce, TimeSpan.FromSeconds(30), Timeout.InfiniteTimeSpan, + maxConnectionsPerServer: 1))); + var options = CreateOptions(); + var endpoint = CreateEndpoint(HttpVersion.Version20); + + // acquire1: EstablishAsync fails → OnFailed → tcs1 faulted → ServeNextPending + var task1 = TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + + // Small delay so acquire1's Establish increments Establishing before acquire2 arrives + await Task.Delay(10, TestContext.Current.CancellationToken); + + // acquire2: queued (establishing=1, max=1) → served after OnFailed cascades + var task2 = TcpConnectionManagerActor.AcquireAsync(actor, options, endpoint, + TestContext.Current.CancellationToken); + + await Assert.ThrowsAnyAsync(() => task1); + + var lease = await task2; + Assert.NotNull(lease); + Assert.True(lease.IsAlive); + + lease.Dispose(); + } + [Fact(Timeout = 5000)] public async Task Evicted_idle_connection_should_not_be_reused() { diff --git a/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionStageSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionStageSpec.cs new file mode 100644 index 000000000..ca26a168d --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpConnectionStageSpec.cs @@ -0,0 +1,61 @@ +using Akka.Actor; +using Akka.Streams; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; + +namespace Servus.Akka.Tests.IO.Tcp; + +public sealed class TcpConnectionStageSpec +{ + [Fact(Timeout = 5000)] + public void TcpConnectionStage_should_have_correct_shape() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.NotNull(stage.Shape); + Assert.Single(stage.Shape.Inlets); + Assert.Single(stage.Shape.Outlets); + } + + [Fact(Timeout = 5000)] + public void TcpConnectionStage_inlet_should_be_named_correctly() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.Equal("TcpConnection.In", stage.Shape.Inlet.Name); + } + + [Fact(Timeout = 5000)] + public void TcpConnectionStage_outlet_should_be_named_correctly() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.Equal("TcpConnection.Out", stage.Shape.Outlet.Name); + } + + [Fact(Timeout = 5000)] + public void TcpConnectionStage_inlet_should_accept_output_items() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.IsType>(stage.Shape.Inlet); + } + + [Fact(Timeout = 5000)] + public void TcpConnectionStage_outlet_should_produce_input_items() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.IsType>(stage.Shape.Outlet); + } + + [Fact(Timeout = 5000)] + public void TcpConnectionStage_shape_should_have_flow_shape() + { + var stage = new TcpConnectionStage(ActorRefs.Nobody); + + Assert.NotNull(stage.Shape); + Assert.Equal(stage.Shape.Inlet, stage.Shape.Inlets[0]); + Assert.Equal(stage.Shape.Outlet, stage.Shape.Outlets[0]); + } +} diff --git a/src/TurboHTTP.Tests/Transport/TcpOptionsSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpOptionsSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/TcpOptionsSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpOptionsSpec.cs index a7d7213b5..ba78c3d45 100644 --- a/src/TurboHTTP.Tests/Transport/TcpOptionsSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpOptionsSpec.cs @@ -1,7 +1,7 @@ using System.Net; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpOptionsSpec { diff --git a/src/Servus.Akka.Tests/IO/Tcp/TcpPumpManagerSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpPumpManagerSpec.cs new file mode 100644 index 000000000..a8dd81b2c --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpPumpManagerSpec.cs @@ -0,0 +1,148 @@ +using System.Buffers; +using System.Net; +using System.Threading.Channels; +using Akka.Actor; +using Akka.TestKit.Xunit; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; + +namespace Servus.Akka.Tests.IO.Tcp; + +public sealed class TcpPumpManagerSpec : TestKit +{ + private static readonly RequestEndpoint TestEndpoint = new() + { + Scheme = "http", + Host = "localhost", + Port = 8080, + Version = HttpVersion.Version11 + }; + + private static (Channel inbound, ConnectionHandle handle) CreateTestHandle() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, TestEndpoint); + return (inbound, handle); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundComplete_CleanClose_when_channel_completes_normally() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + inbound.Writer.TryComplete(); + pump.StartInboundPump(handle, TestEndpoint, gen: 1); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(TlsCloseKind.CleanClose, msg.CloseKind); + Assert.Equal(1, msg.Gen); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundComplete_AbruptClose_when_channel_closed_with_inner_AbruptCloseException() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // TryComplete(AbruptCloseException) → WaitToReadAsync throws ChannelClosedException(AbruptCloseException) + inbound.Writer.TryComplete(new AbruptCloseException()); + pump.StartInboundPump(handle, TestEndpoint, gen: 2); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(TlsCloseKind.AbruptClose, msg.CloseKind); + Assert.Equal(2, msg.Gen); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_send_InboundPumpFailed_on_unexpected_exception() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // Non-AbruptClose exception → ChannelClosedException(IOException) → caught by catch(Exception) + inbound.Writer.TryComplete(new IOException("unexpected I/O error")); + pump.StartInboundPump(handle, TestEndpoint, gen: 0); + + var msg = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(msg.Error); + } + + [Fact(Timeout = 5000)] + public async Task StopInboundPump_should_cancel_pump_and_send_no_messages() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + var (_, handle) = CreateTestHandle(); + + pump.StartInboundPump(handle, TestEndpoint, gen: 0); + pump.StopInboundPump(); + + // Allow some time for any stray messages to arrive + await Task.Delay(150, TestContext.Current.CancellationToken); + await probe.ExpectNoMsgAsync(TimeSpan.Zero, TestContext.Current.CancellationToken); + } + + [Fact(Timeout = 5000)] + public async Task PumpAsync_should_flush_and_grow_batch_when_full() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + var (inbound, handle) = CreateTestHandle(); + + // Detect the actual ArrayPool bucket size for Rent(8) at runtime (may be 8, 16, etc.) + var sampleBatch = ArrayPool.Shared.Rent(8); + var initialBatchSize = sampleBatch.Length; + ArrayPool.Shared.Return(sampleBatch); + + // Write initialBatchSize+1 items: the first initialBatchSize trigger a full-batch flush, + // then item initialBatchSize+1 lands in the grown batch. + // Expected: InboundBatch(initialBatchSize) → InboundBatch(1) → InboundComplete(CleanClose) + for (var i = 0; i < initialBatchSize + 1; i++) + { + await inbound.Writer.WriteAsync(NetworkBuffer.Rent(1), TestContext.Current.CancellationToken); + } + + inbound.Writer.TryComplete(); + pump.StartInboundPump(handle, TestEndpoint, gen: 0); + + var batch1 = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(initialBatchSize, batch1.Count); + + var batch2 = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(1, batch2.Count); + + var complete = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(TlsCloseKind.CleanClose, complete.CloseKind); + } + + [Fact(Timeout = 5000)] + public async Task StartInboundPump_should_cancel_previous_pump_when_called_again() + { + var probe = CreateTestProbe(); + var pump = new TcpPumpManager(probe.Ref); + + var (inbound1, handle1) = CreateTestHandle(); + var (inbound2, handle2) = CreateTestHandle(); + + // Start first pump — channel stays open + pump.StartInboundPump(handle1, TestEndpoint, gen: 1); + + // Start second pump — cancels the first + inbound2.Writer.TryComplete(); + pump.StartInboundPump(handle2, TestEndpoint, gen: 2); + + // Only messages from pump2 expected; pump1 was cancelled + var complete = await probe.ExpectMsgAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(2, complete.Gen); + + // Write to the cancelled pump1 channel — should produce no further messages + await inbound1.Writer.WriteAsync(NetworkBuffer.Rent(1), TestContext.Current.CancellationToken); + await Task.Delay(100, TestContext.Current.CancellationToken); + await probe.ExpectNoMsgAsync(TimeSpan.Zero, TestContext.Current.CancellationToken); + } +} diff --git a/src/Servus.Akka.Tests/IO/Tcp/TcpTransportEventSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportEventSpec.cs new file mode 100644 index 000000000..6b2a4ad75 --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportEventSpec.cs @@ -0,0 +1,129 @@ +using System.Buffers; +using System.Net; +using System.Threading.Channels; +using Akka.Actor; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; + +namespace Servus.Akka.Tests.IO.Tcp; + +public sealed class TcpTransportEventSpec +{ + [Fact(Timeout = 5000)] + public void LeaseAcquired_should_preserve_lease() + { + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + var key = new RequestEndpoint + { + Scheme = "http", + Host = "localhost", + Port = 80, + Version = HttpVersion.Version11 + }; + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, key); + var state = new ClientState(Stream.Null, inbound, outbound); + var lease = new ConnectionLease(handle, state); + + var evt = new LeaseAcquired(lease); + + Assert.Same(lease, evt.Lease); + } + + [Fact(Timeout = 5000)] + public void AcquisitionFailed_should_preserve_error() + { + var ex = new IOException("test"); + var evt = new AcquisitionFailed(ex); + + Assert.Same(ex, evt.Error); + } + + [Fact(Timeout = 5000)] + public void InboundBatch_should_preserve_fields() + { + var batch = ArrayPool.Shared.Rent(8); + var evt = new InboundBatch(batch, 3, 7); + + Assert.Same(batch, evt.Batch); + Assert.Equal(3, evt.Count); + Assert.Equal(7, evt.Gen); + + ArrayPool.Shared.Return(batch); + } + + [Fact(Timeout = 5000)] + public void InboundComplete_should_preserve_fields() + { + var evt = new InboundComplete(TlsCloseKind.AbruptClose, 5); + + Assert.Equal(TlsCloseKind.AbruptClose, evt.CloseKind); + Assert.Equal(5, evt.Gen); + } + + [Fact(Timeout = 5000)] + public void InboundPumpFailed_should_preserve_error() + { + var ex = new IOException("pump error"); + var evt = new InboundPumpFailed(ex); + + Assert.Same(ex, evt.Error); + } + + [Fact(Timeout = 5000)] + public void OutboundWriteDone_should_implement_interface() + { + ITcpTransportEvent evt = new OutboundWriteDone(); + + Assert.IsType(evt); + } + + [Fact(Timeout = 5000)] + public void OutboundWriteFailed_should_preserve_error() + { + var ex = new IOException("write error"); + var evt = new OutboundWriteFailed(ex); + + Assert.Same(ex, evt.Error); + } + + [Fact(Timeout = 5000)] + public void FlushNextCompleted_should_implement_interface() + { + ITcpTransportEvent evt = new FlushNextCompleted(); + + Assert.IsType(evt); + } + + [Fact(Timeout = 5000)] + public void InboundComplete_equality_should_compare_all_fields() + { + var a = new InboundComplete(TlsCloseKind.CleanClose, 1); + var b = new InboundComplete(TlsCloseKind.CleanClose, 1); + var c = new InboundComplete(TlsCloseKind.AbruptClose, 1); + var d = new InboundComplete(TlsCloseKind.CleanClose, 2); + + Assert.Equal(a, b); + Assert.NotEqual(a, c); + Assert.NotEqual(a, d); + } + + [Fact(Timeout = 5000)] + public void OutboundWriteDone_equality_should_match() + { + var a = new OutboundWriteDone(); + var b = new OutboundWriteDone(); + + Assert.Equal(a, b); + } + + [Fact(Timeout = 5000)] + public void FlushNextCompleted_equality_should_match() + { + var a = new FlushNextCompleted(); + var b = new FlushNextCompleted(); + + Assert.Equal(a, b); + } +} diff --git a/src/Servus.Akka.Tests/IO/Tcp/TcpTransportFactorySpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportFactorySpec.cs new file mode 100644 index 000000000..9523f58be --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportFactorySpec.cs @@ -0,0 +1,42 @@ +using Akka.Actor; +using Servus.Akka.IO.Tcp; + +namespace Servus.Akka.Tests.IO.Tcp; + +public sealed class TcpTransportFactorySpec +{ + [Fact(Timeout = 5000)] + public void TcpTransportFactory_should_throw_on_null_connection_manager() + { + Assert.Throws(() => new TcpTransportFactory(null!)); + } + + [Fact(Timeout = 5000)] + public void TcpTransportFactory_should_accept_valid_actor_ref() + { + var factory = new TcpTransportFactory(ActorRefs.Nobody); + + Assert.NotNull(factory); + } + + [Fact(Timeout = 5000)] + public void Create_should_return_non_null_flow() + { + var factory = new TcpTransportFactory(ActorRefs.Nobody); + + var flow = factory.Create(); + + Assert.NotNull(flow); + } + + [Fact(Timeout = 5000)] + public void Create_should_return_independent_flows() + { + var factory = new TcpTransportFactory(ActorRefs.Nobody); + + var flow1 = factory.Create(); + var flow2 = factory.Create(); + + Assert.NotSame(flow1, flow2); + } +} diff --git a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineDataFlowSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineDataFlowSpec.cs similarity index 92% rename from src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineDataFlowSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineDataFlowSpec.cs index 1b0037c53..60d6acf78 100644 --- a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineDataFlowSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineDataFlowSpec.cs @@ -2,13 +2,11 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Protocol.Http11; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpTransportStateMachineDataFlowSpec { @@ -32,7 +30,6 @@ private static (TcpTransportStateMachine Sm, MockTransportOperations Ops) Create var sm = new TcpTransportStateMachine( ops, ActorRefs.Nobody, - new TurboClientOptions(), ActorRefs.Nobody); return (sm, ops); } @@ -220,12 +217,15 @@ public void HandlePush_multiple_acquire_items_should_track_pending() Assert.Equal(0, ops.CompleteStageCount); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.Equal(0, ops.CompleteStageCount); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.Equal(1, ops.CompleteStageCount); } diff --git a/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineEdgeCaseSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineEdgeCaseSpec.cs new file mode 100644 index 000000000..8aeaaa836 --- /dev/null +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineEdgeCaseSpec.cs @@ -0,0 +1,297 @@ +using System.Net; +using System.Threading.Channels; +using Akka.Actor; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; + +namespace Servus.Akka.Tests.IO.Tcp; + +public sealed class TcpTransportStateMachineEdgeCaseSpec +{ + private static readonly RequestEndpoint TestEndpoint = new() + { + Scheme = "http", + Host = "localhost", + Port = 8080, + Version = HttpVersion.Version11 + }; + + private static readonly TcpOptions TestTcpOptions = new() + { + Host = "localhost", + Port = 8080 + }; + + private static (TcpTransportStateMachine Sm, MockTransportOperations Ops) CreateStateMachine() + { + var ops = new MockTransportOperations(); + var sm = new TcpTransportStateMachine( + ops, + ActorRefs.Nobody, + ActorRefs.Nobody); + return (sm, ops); + } + + private static ConnectionLease CreateTestLease(RequestEndpoint? endpoint = null) + { + var key = endpoint ?? TestEndpoint; + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + + var handle = ConnectionHandle.CreateDirect( + outbound.Writer, + inbound.Reader, + key); + + var state = new ClientState( + Stream.Null, + inbound, + outbound); + + return new ConnectionLease(handle, state); + } + + [Fact(Timeout = 5000)] + public void FlushNext_without_handle_should_dispose_orphaned_buffers() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new ConnectItem(TestTcpOptions) { Key = TestEndpoint }); + + var buf1 = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + var buf2 = NetworkBufferTestExtensions.FromArray([4, 5, 6]); + sm.HandlePush(buf1); + sm.HandlePush(buf2); + + sm.HandleDownstreamFinish(); + + sm.Dispatch(new FlushNextCompleted()); + + Assert.True(ops.PullInputCount > 0); + } + + [Fact(Timeout = 5000)] + public void PostStop_with_active_lease_should_dispose_lease() + { + var (sm, _) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + + sm.PostStop(); + + Assert.False(lease.IsAlive); + } + + [Fact(Timeout = 5000)] + public void HandleUpstreamFinish_with_pending_responses_and_writes_should_defer() + { + var (sm, ops) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + + sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); + + sm.HandleUpstreamFinish(); + + Assert.Equal(0, ops.CompleteStageCount); + } + + [Fact(Timeout = 5000)] + public void Reconnect_sequence_should_cleanup_old_and_acquire_new() + { + var (sm, ops) = CreateStateMachine(); + var lease1 = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease1)); + + sm.HandlePush(new ConnectItem(TestTcpOptions) + { Key = TestEndpoint, IsReconnect = true }); + + Assert.False(lease1.IsAlive); + + var lease2 = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease2)); + + Assert.Contains(ops.PushedOutputs, item => item is ConnectedSignalItem); + } + + [Fact(Timeout = 5000)] + public void Multiple_reconnects_should_dispose_intermediate_leases() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new ConnectItem(TestTcpOptions) { Key = TestEndpoint }); + var lease1 = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease1)); + + sm.HandlePush(new ConnectItem(TestTcpOptions) + { Key = TestEndpoint, IsReconnect = true }); + Assert.False(lease1.IsAlive); + + var lease2 = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease2)); + + sm.HandlePush(new ConnectItem(TestTcpOptions) + { Key = TestEndpoint, IsReconnect = true }); + Assert.False(lease2.IsAlive); + + var lease3 = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease3)); + + Assert.True(lease3.IsAlive); + } + + [Fact(Timeout = 5000)] + public void HandleConnectionReuseItem_canReuse_false_should_null_handle() + { + var (sm, ops) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + + sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); + + sm.HandlePush(new ConnectionReuseItem(false) { Key = TestEndpoint }); + + var buf = NetworkBufferTestExtensions.FromArray([1, 2, 3]); + sm.HandlePush(buf); + + Assert.True(ops.PullInputCount > 0); + } + + [Fact(Timeout = 5000)] + public void OnTimer_null_key_should_be_ignored() + { + var (sm, ops) = CreateStateMachine(); + + sm.OnTimer(null); + + Assert.Empty(ops.PushedOutputs); + Assert.Equal(0, ops.CompleteStageCount); + } + + [Fact(Timeout = 5000)] + public void HandlePush_MaxConcurrentStreamsItem_without_lease_should_pull() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new MaxConcurrentStreamsItem(42) { Key = TestEndpoint }); + + Assert.True(ops.PullInputCount > 0); + } + + [Fact(Timeout = 5000)] + public void HandlePush_StreamAcquireItem_without_lease_should_pull() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); + + Assert.True(ops.PullInputCount > 0); + } + + [Fact(Timeout = 5000)] + public void Dispatch_AcquisitionFailed_without_pending_connect_should_not_push() + { + var (sm, ops) = CreateStateMachine(); + + sm.Dispatch(new AcquisitionFailed(new IOException("no pending connect"))); + + Assert.Empty(ops.PushedOutputs); + } + + [Fact(Timeout = 5000)] + public void HandleUpstreamFinish_after_no_reuse_should_complete_stage() + { + var (sm, ops) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + + sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); + sm.HandlePush(new ConnectionReuseItem(false) { Key = TestEndpoint }); + + sm.HandleUpstreamFinish(); + + Assert.Equal(1, ops.CompleteStageCount); + } + + [Fact(Timeout = 5000)] + public void Dispatch_InboundComplete_clean_close_should_signal_and_pull() + { + var (sm, ops) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + ops.PushedOutputs.Clear(); + var pullBefore = ops.PullInputCount; + + sm.Dispatch(new InboundComplete(TlsCloseKind.CleanClose, 1)); + + Assert.Contains(ops.PushedOutputs, item => item is CloseSignalItem { CloseKind: TlsCloseKind.CleanClose }); + Assert.True(ops.PullInputCount > pullBefore); + } + + [Fact(Timeout = 5000)] + public void Dispatch_InboundComplete_abrupt_close_should_signal_and_pull() + { + var (sm, ops) = CreateStateMachine(); + var lease = CreateTestLease(); + sm.Dispatch(new LeaseAcquired(lease)); + ops.PushedOutputs.Clear(); + var pullBefore = ops.PullInputCount; + + sm.Dispatch(new InboundComplete(TlsCloseKind.AbruptClose, 1)); + + Assert.Contains(ops.PushedOutputs, item => item is CloseSignalItem { CloseKind: TlsCloseKind.AbruptClose }); + Assert.True(ops.PullInputCount > pullBefore); + } + + [Fact(Timeout = 5000)] + public void HandlePush_ConnectItem_with_zero_timeout_should_use_default() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new ConnectItem(new TcpOptions + { + Host = "localhost", + Port = 8080, + ConnectTimeout = TimeSpan.Zero + }) + { Key = TestEndpoint }); + + var timer = Assert.Single(ops.ScheduledTimers, t => t.Key == "connect-timeout"); + Assert.Equal(TimeSpan.FromSeconds(10), timer.Delay); + } + + [Fact(Timeout = 5000)] + public void HandlePush_ConnectItem_with_custom_timeout_should_use_it() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new ConnectItem(new TcpOptions + { + Host = "localhost", + Port = 8080, + ConnectTimeout = TimeSpan.FromSeconds(30) + }) + { Key = TestEndpoint }); + + var timer = Assert.Single(ops.ScheduledTimers, t => t.Key == "connect-timeout"); + Assert.Equal(TimeSpan.FromSeconds(30), timer.Delay); + } + + [Fact(Timeout = 5000)] + public void HandlePush_ConnectItem_with_negative_timeout_should_use_default() + { + var (sm, ops) = CreateStateMachine(); + + sm.HandlePush(new ConnectItem(new TcpOptions + { + Host = "localhost", + Port = 8080, + ConnectTimeout = TimeSpan.FromSeconds(-1) + }) + { Key = TestEndpoint }); + + var timer = Assert.Single(ops.ScheduledTimers, t => t.Key == "connect-timeout"); + Assert.Equal(TimeSpan.FromSeconds(10), timer.Delay); + } +} diff --git a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineErrorSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineErrorSpec.cs similarity index 97% rename from src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineErrorSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineErrorSpec.cs index 3aefd4d60..e759220b9 100644 --- a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineErrorSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineErrorSpec.cs @@ -3,12 +3,11 @@ using System.Net.Sockets; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpTransportStateMachineErrorSpec { @@ -32,7 +31,6 @@ private static (TcpTransportStateMachine Sm, MockTransportOperations Ops) Create var sm = new TcpTransportStateMachine( ops, ActorRefs.Nobody, - new TurboClientOptions(), ActorRefs.Nobody); return (sm, ops); } diff --git a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineLifecycleSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineLifecycleSpec.cs similarity index 85% rename from src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineLifecycleSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineLifecycleSpec.cs index e46ee8521..60dbd52a3 100644 --- a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineLifecycleSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineLifecycleSpec.cs @@ -1,13 +1,11 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Protocol.Http11; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpTransportStateMachineLifecycleSpec { @@ -33,7 +31,6 @@ private static (TcpTransportStateMachine Sm, MockTransportOperations Ops) Create var sm = new TcpTransportStateMachine( ops, ActorRefs.Nobody, - new TurboClientOptions(), ActorRefs.Nobody); return (sm, ops); } @@ -62,7 +59,8 @@ public void Dispatch_LeaseAcquired_during_reconnect_should_push_connected_signal { var (sm, ops) = CreateStateMachine(); - sm.HandlePush(new ReconnectItem { Key = TestEndpoint }); + sm.HandlePush(new ConnectItem(new TcpOptions { Host = TestEndpoint.Host, Port = TestEndpoint.Port }) + { Key = TestEndpoint, IsReconnect = true }); ops.PushedOutputs.Clear(); var lease = CreateTestLease(); @@ -120,7 +118,8 @@ public void HandleConnectionReuseItem_canReuse_true_with_multiple_pending_should sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); var pullBefore = ops.PullInputCount; - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.True(ops.PullInputCount > pullBefore); Assert.Equal(0, ops.CompleteStageCount); @@ -136,7 +135,8 @@ public void HandleConnectionReuseItem_canReuse_true_with_single_pending_should_m sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); var pullBefore = ops.PullInputCount; - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.True(ops.PullInputCount > pullBefore); Assert.Equal(0, ops.CompleteStageCount); @@ -154,7 +154,8 @@ public void HandleConnectionReuseItem_canReuse_true_with_upstream_finished_shoul Assert.Equal(0, ops.CompleteStageCount); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.Equal(1, ops.CompleteStageCount); } @@ -171,7 +172,8 @@ public void HandleConnectionReuseItem_canReuse_false_with_upstream_finished_shou Assert.Equal(0, ops.CompleteStageCount); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.Close("server close")) { Key = TestEndpoint }); + sm.HandlePush(new ConnectionReuseItem(false) + { Key = TestEndpoint }); Assert.Equal(1, ops.CompleteStageCount); } @@ -181,22 +183,25 @@ public void AutoConnect_with_different_endpoint_should_trigger_acquire() { var (sm, ops) = CreateStateMachine(); - var buffer = NetworkBufferTestExtensions.FromArray([1, 2, 3]); - buffer.Key = AltEndpoint; - sm.HandlePush(buffer); + sm.HandlePush(new ConnectItem + { + Key = AltEndpoint, + Options = new TcpOptions { Host = AltEndpoint.Host, Port = AltEndpoint.Port } + }); Assert.Contains(ops.ScheduledTimers, t => t.Key == "connect-timeout"); } [Fact(Timeout = 5000)] - public void ReconnectItem_should_teardown_and_acquire() + public void ConnectItem_with_IsReconnect_should_teardown_and_acquire() { var (sm, ops) = CreateStateMachine(); var lease1 = CreateTestLease(); sm.Dispatch(new LeaseAcquired(lease1)); ops.PushedOutputs.Clear(); - sm.HandlePush(new ReconnectItem { Key = AltEndpoint }); + sm.HandlePush(new ConnectItem(new TcpOptions { Host = AltEndpoint.Host, Port = AltEndpoint.Port }) + { Key = AltEndpoint, IsReconnect = true }); Assert.Contains(ops.ScheduledTimers, t => t.Key == "connect-timeout"); Assert.False(lease1.IsAlive); diff --git a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineSpec.cs similarity index 92% rename from src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineSpec.cs index 57b18115e..5ab8fc502 100644 --- a/src/TurboHTTP.StreamTests/Transport/TcpTransportStateMachineSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TcpTransportStateMachineSpec.cs @@ -2,14 +2,11 @@ using System.Net; using System.Threading.Channels; using Akka.Actor; -using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Protocol.Http11; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; +using Servus.Akka.Tests.Utils; -namespace TurboHTTP.StreamTests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TcpTransportStateMachineSpec { @@ -33,7 +30,6 @@ private static (TcpTransportStateMachine Sm, MockTransportOperations Ops) Create var sm = new TcpTransportStateMachine( ops, ActorRefs.Nobody, - new TurboClientOptions(), ActorRefs.Nobody); return (sm, ops); } @@ -207,7 +203,8 @@ public void HandlePush_ConnectionReuseItem_canReuse_true_should_pull() sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); var pullBefore = ops.PullInputCount; - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.True(ops.PullInputCount > pullBefore); } @@ -222,7 +219,8 @@ public void HandlePush_ConnectionReuseItem_canReuse_false_should_teardown_and_pu sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); var pullBefore = ops.PullInputCount; - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.Close("server close")) { Key = TestEndpoint }); + sm.HandlePush(new ConnectionReuseItem(false) + { Key = TestEndpoint }); Assert.True(ops.PullInputCount > pullBefore); } @@ -289,7 +287,8 @@ public void HandleUpstreamFinish_with_pending_responses_should_defer_complete() Assert.Equal(0, ops.CompleteStageCount); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); Assert.Equal(1, ops.CompleteStageCount); } @@ -395,15 +394,16 @@ public void HandleDownstreamFinish_should_cleanup_transport() } [Fact(Timeout = 5000)] - public void AutoConnect_should_trigger_on_first_data_item() + public void HandlePush_data_before_ConnectItem_should_buffer_and_signal_pull() { var (sm, ops) = CreateStateMachine(); var buffer = NetworkBufferTestExtensions.FromArray([1, 2, 3]); buffer.Key = TestEndpoint; + sm.HandlePush(buffer); - Assert.Contains(ops.ScheduledTimers, t => t.Key == "connect-timeout"); + Assert.True(ops.PullInputCount > 0); } [Fact(Timeout = 5000)] @@ -416,8 +416,10 @@ public void Multiple_StreamAcquire_then_Reuse_should_complete_all() sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); sm.HandlePush(new StreamAcquireItem { Key = TestEndpoint }); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); - sm.HandlePush(new ConnectionReuseItem(ConnectionReuseDecision.KeepAlive("test")) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); + sm.HandlePush( + new ConnectionReuseItem(true) { Key = TestEndpoint }); sm.HandleUpstreamFinish(); Assert.Equal(1, ops.CompleteStageCount); diff --git a/src/TurboHTTP.Tests/Transport/TlsClientProviderSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TlsClientProviderSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/TlsClientProviderSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TlsClientProviderSpec.cs index e82adf73c..c2ccd7066 100644 --- a/src/TurboHTTP.Tests/Transport/TlsClientProviderSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TlsClientProviderSpec.cs @@ -1,8 +1,9 @@ using System.Net; using System.Text; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TlsClientProviderSpec { diff --git a/src/TurboHTTP.Tests/Transport/TlsOptionsSpec.cs b/src/Servus.Akka.Tests/IO/Tcp/TlsOptionsSpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/TlsOptionsSpec.cs rename to src/Servus.Akka.Tests/IO/Tcp/TlsOptionsSpec.cs index 5259bfc68..7e44cd9e4 100644 --- a/src/TurboHTTP.Tests/Transport/TlsOptionsSpec.cs +++ b/src/Servus.Akka.Tests/IO/Tcp/TlsOptionsSpec.cs @@ -2,9 +2,9 @@ using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Tests.Transport; +namespace Servus.Akka.Tests.IO.Tcp; public sealed class TlsOptionsSpec { diff --git a/src/Servus.Akka.Tests/Servus.Akka.Tests.csproj b/src/Servus.Akka.Tests/Servus.Akka.Tests.csproj new file mode 100644 index 000000000..23e8e324a --- /dev/null +++ b/src/Servus.Akka.Tests/Servus.Akka.Tests.csproj @@ -0,0 +1,22 @@ + + + + Exe + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Servus.Akka.Tests/Utils/FailOnceConnectionFactory.cs b/src/Servus.Akka.Tests/Utils/FailOnceConnectionFactory.cs new file mode 100644 index 000000000..eb89369d2 --- /dev/null +++ b/src/Servus.Akka.Tests/Utils/FailOnceConnectionFactory.cs @@ -0,0 +1,29 @@ +using System.Threading.Channels; +using Servus.Akka.IO; + +namespace Servus.Akka.Tests.Utils; + +/// +/// A test factory that fails the first EstablishAsync call, then succeeds on all subsequent calls. +/// Used to verify that connection-establishment failures trigger ServeNextPending cascades. +/// +internal sealed class FailOnceConnectionFactory : IConnectionFactory +{ + private int _callCount; + + public Task EstablishAsync(ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + if (Interlocked.Increment(ref _callCount) == 1) + { + return Task.FromException(new IOException("Simulated first-call connection failure")); + } + + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, endpoint); + var state = new ClientState(Stream.Null, inbound, outbound); + return Task.FromResult(new ConnectionLease(handle, state)); + } +} diff --git a/src/TurboHTTP.Tests.Shared/FakeClientProvider.cs b/src/Servus.Akka.Tests/Utils/FakeClientProvider.cs similarity index 89% rename from src/TurboHTTP.Tests.Shared/FakeClientProvider.cs rename to src/Servus.Akka.Tests/Utils/FakeClientProvider.cs index a81089d45..6c3ecd43e 100644 --- a/src/TurboHTTP.Tests.Shared/FakeClientProvider.cs +++ b/src/Servus.Akka.Tests/Utils/FakeClientProvider.cs @@ -1,7 +1,7 @@ using System.Net; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; -namespace TurboHTTP.Tests.Shared; +namespace Servus.Akka.Tests.Utils; internal sealed class FakeClientProvider(bool blockGetStream = false, byte[]? inboundBytes = null) : IClientProvider @@ -37,7 +37,8 @@ public Task AcceptInboundStreamAsync(CancellationToken ct = default) { if (inboundBytes is not null) { - return Task.FromResult(new MemoryStream(inboundBytes)); + var bytes = inboundBytes; + return Task.Run(() => new MemoryStream(bytes), ct); } return Task.Delay(Timeout.Infinite, ct).ContinueWith(_ => diff --git a/src/TurboHTTP.Tests.Shared/InMemoryConnectionFactory.cs b/src/Servus.Akka.Tests/Utils/InMemoryConnectionFactory.cs similarity index 82% rename from src/TurboHTTP.Tests.Shared/InMemoryConnectionFactory.cs rename to src/Servus.Akka.Tests/Utils/InMemoryConnectionFactory.cs index d4fdd8873..03ca60621 100644 --- a/src/TurboHTTP.Tests.Shared/InMemoryConnectionFactory.cs +++ b/src/Servus.Akka.Tests/Utils/InMemoryConnectionFactory.cs @@ -1,8 +1,7 @@ using System.Threading.Channels; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; -namespace TurboHTTP.Tests.Shared; +namespace Servus.Akka.Tests.Utils; internal sealed class InMemoryConnectionFactory : IConnectionFactory { @@ -10,7 +9,7 @@ internal sealed class InMemoryConnectionFactory : IConnectionFactory public IReadOnlyList EstablishedLeases => _established; - public Task EstablishAsync(TcpOptions options, RequestEndpoint endpoint, CancellationToken ct) + public Task EstablishAsync(ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct) { ct.ThrowIfCancellationRequested(); diff --git a/src/TurboHTTP.Tests.Shared/InMemoryQuicConnectionFactory.cs b/src/Servus.Akka.Tests/Utils/InMemoryQuicConnectionFactory.cs similarity index 87% rename from src/TurboHTTP.Tests.Shared/InMemoryQuicConnectionFactory.cs rename to src/Servus.Akka.Tests/Utils/InMemoryQuicConnectionFactory.cs index fe84a4a3c..1b57d9c9d 100644 --- a/src/TurboHTTP.Tests.Shared/InMemoryQuicConnectionFactory.cs +++ b/src/Servus.Akka.Tests/Utils/InMemoryQuicConnectionFactory.cs @@ -1,9 +1,9 @@ -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; #pragma warning disable CA1416 -namespace TurboHTTP.Tests.Shared; +namespace Servus.Akka.Tests.Utils; internal sealed class InMemoryQuicConnectionFactory : IQuicConnectionFactory { diff --git a/src/Servus.Akka.Tests/Utils/MockTransportOperations.cs b/src/Servus.Akka.Tests/Utils/MockTransportOperations.cs new file mode 100644 index 000000000..116fe3128 --- /dev/null +++ b/src/Servus.Akka.Tests/Utils/MockTransportOperations.cs @@ -0,0 +1,21 @@ +using Akka.Event; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; + +namespace Servus.Akka.Tests.Utils; + +internal sealed class MockTransportOperations : ITransportOperations +{ + public List PushedOutputs { get; } = []; + public int PullInputCount { get; set; } + public int CompleteStageCount { get; private set; } + public List<(string Key, TimeSpan Delay)> ScheduledTimers { get; } = []; + public List CancelledTimers { get; } = []; + + public void OnPushOutput(IInputItem item) => PushedOutputs.Add(item); + public void OnSignalPullInput() => PullInputCount++; + public void OnCompleteStage() => CompleteStageCount++; + public void OnScheduleTimer(string key, TimeSpan delay) => ScheduledTimers.Add((key, delay)); + public void OnCancelTimer(string key) => CancelledTimers.Add(key); + public ILoggingAdapter Log => NoLogger.Instance; +} \ No newline at end of file diff --git a/src/Servus.Akka.Tests/Utils/NetworkBufferTestExtensions.cs b/src/Servus.Akka.Tests/Utils/NetworkBufferTestExtensions.cs new file mode 100644 index 000000000..a5c588d80 --- /dev/null +++ b/src/Servus.Akka.Tests/Utils/NetworkBufferTestExtensions.cs @@ -0,0 +1,15 @@ +using Servus.Akka.IO; + +namespace Servus.Akka.Tests.Utils; + +public static class NetworkBufferTestExtensions +{ + internal static NetworkBuffer FromArray(byte[] data, int length = -1) + { + var len = length < 0 ? data.Length : length; + var buf = NetworkBuffer.Rent(len); + data.AsSpan(0, len).CopyTo(buf.FullMemory.Span); + buf.Length = len; + return buf; + } +} \ No newline at end of file diff --git a/src/Servus.Akka.Tests/Utils/SlowConnectionFactory.cs b/src/Servus.Akka.Tests/Utils/SlowConnectionFactory.cs new file mode 100644 index 000000000..edf984369 --- /dev/null +++ b/src/Servus.Akka.Tests/Utils/SlowConnectionFactory.cs @@ -0,0 +1,24 @@ +using System.Threading.Channels; +using Servus.Akka.IO; + +namespace Servus.Akka.Tests.Utils; + +/// +/// A test factory that intentionally ignores the cancellation token during establish, simulating +/// a slow network that completes after the caller has already cancelled their request. +/// Used to exercise the OnEstablished path where TrySetResult returns false. +/// +internal sealed class SlowConnectionFactory(TimeSpan delay) : IConnectionFactory +{ + public async Task EstablishAsync(ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct) + { + // Deliberately ignore ct — simulates a slow network that doesn't respect cancellation. + await Task.Delay(delay, CancellationToken.None).ConfigureAwait(false); + + var inbound = Channel.CreateUnbounded(); + var outbound = Channel.CreateUnbounded(); + var handle = ConnectionHandle.CreateDirect(outbound.Writer, inbound.Reader, endpoint); + var state = new ClientState(Stream.Null, inbound, outbound); + return new ConnectionLease(handle, state); + } +} diff --git a/src/Servus.Akka.Tests/Utils/SlowQuicConnectionFactory.cs b/src/Servus.Akka.Tests/Utils/SlowQuicConnectionFactory.cs new file mode 100644 index 000000000..49f4eaee5 --- /dev/null +++ b/src/Servus.Akka.Tests/Utils/SlowQuicConnectionFactory.cs @@ -0,0 +1,24 @@ +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; + +#pragma warning disable CA1416 + +namespace Servus.Akka.Tests.Utils; + +/// +/// A test QUIC factory that intentionally ignores the cancellation token during establish, +/// simulating a slow network that completes after the caller has already cancelled their request. +/// Used to exercise the OnEstablished path where TrySetResult returns false. +/// +internal sealed class SlowQuicConnectionFactory(TimeSpan delay) : IQuicConnectionFactory +{ + public async Task EstablishAsync(QuicOptions options, RequestEndpoint endpoint, CancellationToken ct) + { + // Deliberately ignore ct — simulates a slow network that doesn't respect cancellation. + await Task.Delay(delay, CancellationToken.None).ConfigureAwait(false); + + var provider = new FakeClientProvider(); + var handle = new QuicConnectionHandle(provider, options, endpoint); + return new QuicConnectionLease(handle); + } +} diff --git a/src/Servus.Akka/Diagnostics/IServusTraceListener.cs b/src/Servus.Akka/Diagnostics/IServusTraceListener.cs new file mode 100644 index 000000000..f64bebc86 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/IServusTraceListener.cs @@ -0,0 +1,21 @@ +namespace Servus.Akka.Diagnostics; + +/// +/// Receives trace events from . +/// Implementations must be thread-safe. +/// +public interface IServusTraceListener +{ + /// + /// Returns when this listener wants events + /// at the given in the given . + /// Called inside on the hot path. + /// + bool IsEnabled(ServusTraceLevel level, ServusTraceCategory category); + + /// + /// Receives a single trace event. Called only when + /// returned . + /// + void Write(in ServusTraceEvent evt); +} diff --git a/src/Servus.Akka/Diagnostics/LoggerServusTraceListener.cs b/src/Servus.Akka/Diagnostics/LoggerServusTraceListener.cs new file mode 100644 index 000000000..c11c3d8d1 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/LoggerServusTraceListener.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Logging; + +namespace Servus.Akka.Diagnostics; + +/// +/// Routes instances to , +/// creating one per . +/// Logger names follow the pattern Servus.Akka.Trace.{Category}. +/// +internal sealed class LoggerServusTraceListener : IServusTraceListener +{ + private readonly Dictionary _loggers; + private readonly ServusTraceCategory _enabledCategories; + private readonly ServusTraceLevel _minimumLevel; + + public LoggerServusTraceListener( + ILoggerFactory loggerFactory, + ServusTraceCategory categories = ServusTraceCategory.All, + ServusTraceLevel minimumLevel = ServusTraceLevel.Debug) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + + _enabledCategories = categories; + _minimumLevel = minimumLevel; + _loggers = CreateLoggers(loggerFactory); + } + + /// + public bool IsEnabled(ServusTraceLevel level, ServusTraceCategory category) + { + return level >= _minimumLevel && (category & _enabledCategories) != 0; + } + + /// + public void Write(in ServusTraceEvent evt) + { + if (!_loggers.TryGetValue(evt.Category, out var logger)) return; + var logLevel = (LogLevel)evt.Level; + if (!logger.IsEnabled(logLevel)) return; + var message = evt.FormatMessage(); + logger.Log(logLevel, "[{SourceType}#{SourceHash:X8}] {Message}", + evt.SourceType, evt.SourceHash, message); + } + + private static Dictionary CreateLoggers(ILoggerFactory loggerFactory) + { + return new Dictionary + { + [ServusTraceCategory.Connection] = loggerFactory.CreateLogger("Servus.Akka.Trace.Connection"), + [ServusTraceCategory.Dns] = loggerFactory.CreateLogger("Servus.Akka.Trace.Dns"), + [ServusTraceCategory.Tls] = loggerFactory.CreateLogger("Servus.Akka.Trace.Tls"), + [ServusTraceCategory.Pool] = loggerFactory.CreateLogger("Servus.Akka.Trace.Pool"), + }; + } +} diff --git a/src/Servus.Akka/Diagnostics/ServusInstrumentation.cs b/src/Servus.Akka/Diagnostics/ServusInstrumentation.cs new file mode 100644 index 000000000..a56325657 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusInstrumentation.cs @@ -0,0 +1,149 @@ +using System.Diagnostics; +using System.Reflection; + +namespace Servus.Akka.Diagnostics; + +/// +/// OpenTelemetry tracing for the Servus.Akka transport layer. +/// Emits spans for connection establishment, DNS resolution, socket connect, +/// TLS handshake, and connection pool wait times. +/// Consumers subscribe via AddSource("Servus.Akka") in the OTel SDK. +/// +public static class ServusInstrumentation +{ + public const string SourceName = "Servus.Akka"; + + private static readonly string Version = + typeof(ServusInstrumentation).Assembly + .GetCustomAttribute()?.InformationalVersion + ?? typeof(ServusInstrumentation).Assembly.GetName().Version?.ToString() + ?? "0.0.0"; + + public static ActivitySource Source { get; } = new(SourceName, Version); + + public static Activity? StartConnect(Uri uri) + { + if (!Source.HasListeners()) + { + return null; + } + + var activity = Source.StartActivity($"{SourceName}.Connect", ActivityKind.Client); + if (activity is null) + { + return null; + } + + activity.SetTag("server.address", uri.Host); + activity.SetTag("server.port", uri.Port); + activity.SetTag("url.scheme", uri.Scheme); + + return activity; + } + + public static Activity? StartDnsLookup(string hostname) + { + if (!Source.HasListeners()) + { + return null; + } + + var activity = Source.StartActivity($"{SourceName}.DnsLookup", ActivityKind.Client); + if (activity is null) + { + return null; + } + + activity.SetTag("dns.question.name", hostname); + + return activity; + } + + /// The peer IP address (e.g., "93.184.216.34"). + /// The peer port number. + /// The transport protocol: "tcp", "udp", or "unix". + /// The network type: "ipv4" or "ipv6". Null for non-IP transports. + public static Activity? StartSocketConnect(string address, int port, + string transport = "tcp", string? networkType = null) + { + if (!Source.HasListeners()) + { + return null; + } + + var activity = Source.StartActivity($"{SourceName}.SocketConnect", ActivityKind.Client); + if (activity is null) + { + return null; + } + + activity.SetTag("network.peer.address", address); + activity.SetTag("network.peer.port", port); + activity.SetTag("network.transport", transport); + if (networkType is not null) + { + activity.SetTag("network.type", networkType); + } + + return activity; + } + + public static Activity? StartTlsHandshake(string host) + { + if (!Source.HasListeners()) + { + return null; + } + + var activity = Source.StartActivity($"{SourceName}.TlsHandshake", ActivityKind.Client); + if (activity is null) + { + return null; + } + + activity.SetTag("server.address", host); + + return activity; + } + + public static Activity? StartWaitForConnection(string address, int port) + { + if (!Source.HasListeners()) + { + return null; + } + + var activity = Source.StartActivity($"{SourceName}.WaitForConnection", ActivityKind.Client); + if (activity is null) + { + return null; + } + + activity.SetTag("server.address", address); + activity.SetTag("server.port", port); + + return activity; + } + + public static void SetTlsInfo(Activity activity, string protocolName, string protocolVersion) + { + activity.SetTag("tls.protocol.name", protocolName); + activity.SetTag("tls.protocol.version", protocolVersion); + } + + public static void SetDnsAnswers(Activity activity, string[] answers) + { + activity.SetTag("dns.answers", answers); + } + + public static void SetNetworkPeerAddress(Activity activity, string address) + { + activity.SetTag("network.peer.address", address); + } + + public static void SetError(Activity activity, Exception exception) + { + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + activity.SetTag("error.type", exception.GetType().FullName); + } +} \ No newline at end of file diff --git a/src/Servus.Akka/Diagnostics/ServusMetrics.cs b/src/Servus.Akka/Diagnostics/ServusMetrics.cs new file mode 100644 index 000000000..fd78b4e17 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusMetrics.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.Metrics; +using System.Reflection; + +namespace Servus.Akka.Diagnostics; + +/// +/// OpenTelemetry metrics for the Servus.Akka transport layer. +/// Tracks connection lifecycle, DNS lookups, and connection pool wait times. +/// Consumers subscribe via AddMeter("Servus.Akka") in the OTel SDK. +/// +public static class ServusMetrics +{ + public const string MeterName = "Servus.Akka"; + + private static readonly string Version = + typeof(ServusMetrics).Assembly + .GetCustomAttribute()?.InformationalVersion + ?? typeof(ServusMetrics).Assembly.GetName().Version?.ToString() + ?? "0.0.0"; + + public static Meter Meter { get; } = new(MeterName, Version); + + /// + /// Number of open connections. + /// Tags: http.connection.state ("active" or "idle"), + /// server.address, server.port. + /// + public static UpDownCounter OpenConnections { get; } = + Meter.CreateUpDownCounter( + "http.client.open_connections", + unit: "{connection}", + description: "Number of currently open transport connections"); + + /// + /// Connection lifetime in seconds. + /// Tags: server.address, server.port. + /// + public static Histogram ConnectionDuration { get; } = + Meter.CreateHistogram( + "http.client.connection.duration", + unit: "s", + description: "Duration of transport connections in seconds"); + + /// + /// Time spent waiting for an available connection from the pool, in seconds. + /// Tags: server.address, server.port. + /// + public static Histogram RequestTimeInQueue { get; } = + Meter.CreateHistogram( + "http.client.request.time_in_queue", + unit: "s", + description: "Time spent waiting for a connection from the pool"); + + /// + /// Duration of DNS lookups, in seconds. + /// Tags: dns.question.name, error.type (if failed). + /// + public static Histogram DnsLookupDuration { get; } = + Meter.CreateHistogram( + "dns.lookup.duration", + unit: "s", + description: "Duration of DNS lookups"); +} diff --git a/src/Servus.Akka/Diagnostics/ServusTrace.cs b/src/Servus.Akka/Diagnostics/ServusTrace.cs new file mode 100644 index 000000000..7c1d76b6c --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusTrace.cs @@ -0,0 +1,379 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Servus.Akka.Diagnostics; + +/// +/// Static API for zero-cost developer tracing. When no listener is configured, +/// trace calls are no-ops (single null-check + inlined branch). +/// is called once at startup before any worker threads exist, +/// so the thread-creation happens-before guarantees visibility without barriers. +/// +internal static class ServusTrace +{ + private static TraceConfig? _config; + + /// + /// Enables tracing with the specified listener, category filter, and minimum level. + /// Must be called before the Akka actor system starts — thread creation provides + /// happens-before visibility to all worker threads. + /// + public static void Configure( + IServusTraceListener listener, + ServusTraceCategory categories = ServusTraceCategory.All, + ServusTraceLevel minimumLevel = ServusTraceLevel.Trace) + { + _config = new TraceConfig(listener, categories, minimumLevel); + } + + /// + /// Disables tracing. All subsequent trace calls become no-ops. + /// + public static void Disable() + { + _config = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool ShouldTrace(ServusTraceCategory category, ServusTraceLevel level) + { + var cfg = _config; + return cfg is not null && cfg.Listener.IsEnabled(level, category) + && (cfg.EnabledCategories & category) != 0 + && level >= cfg.MinimumLevel; + } + + internal static void WriteEvent(in ServusTraceEvent evt) + { + _config?.Listener.Write(in evt); + } + + private sealed record TraceConfig( + IServusTraceListener Listener, + ServusTraceCategory EnabledCategories, + ServusTraceLevel MinimumLevel); + + /// Trace category for TCP/QUIC connection lifecycle events. + public static class Connection + { + private const ServusTraceCategory Category = ServusTraceCategory.Connection; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Trace(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Trace(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Debug(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Debug(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Info(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Warning(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Error(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + } + + /// Trace category for DNS resolution events. + public static class Dns + { + private const ServusTraceCategory Category = ServusTraceCategory.Dns; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Trace(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Trace(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Debug(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Debug(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Info(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Warning(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Error(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + } + + /// Trace category for TLS handshake events. + public static class Tls + { + private const ServusTraceCategory Category = ServusTraceCategory.Tls; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Trace(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Trace(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Debug(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Debug(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Info(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Warning(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Error(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + } + + /// Trace category for connection pool lifecycle events. + public static class Pool + { + private const ServusTraceCategory Category = ServusTraceCategory.Pool; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Trace(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Trace(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Trace)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Trace, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Debug(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Debug(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Debug)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Debug, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Info(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Info(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Info)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Info, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Warning(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Warning(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Warning)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Warning, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Error(object source, string message) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message)); + } + + public static void Error(object source, string message, params object?[] args) + { + if (!ShouldTrace(Category, ServusTraceLevel.Error)) return; + WriteEvent(new ServusTraceEvent(Stopwatch.GetTimestamp(), ServusTraceLevel.Error, Category, + source.GetType().Name, source.GetHashCode(), message, args)); + } + } +} diff --git a/src/Servus.Akka/Diagnostics/ServusTraceCategory.cs b/src/Servus.Akka/Diagnostics/ServusTraceCategory.cs new file mode 100644 index 000000000..dec55f2ac --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusTraceCategory.cs @@ -0,0 +1,16 @@ +namespace Servus.Akka.Diagnostics; + +/// +/// Trace categories for the Servus.Akka transport layer. +/// Powers of 2 enable bitwise combination for filtering. +/// +[Flags] +public enum ServusTraceCategory : byte +{ + None = 0, + Connection = 1, + Dns = 2, + Tls = 4, + Pool = 8, + All = 15, +} \ No newline at end of file diff --git a/src/Servus.Akka/Diagnostics/ServusTraceEvent.cs b/src/Servus.Akka/Diagnostics/ServusTraceEvent.cs new file mode 100644 index 000000000..b2193d7d9 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusTraceEvent.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; + +namespace Servus.Akka.Diagnostics; + +/// +/// Immutable trace event with deferred message formatting. +/// Stores the template and arguments; +/// allocates a formatted string only when called. +/// +public readonly struct ServusTraceEvent +{ + /// Timestamp from . + public long TimestampTicks { get; } + + /// Severity level of this event. + public ServusTraceLevel Level { get; } + + /// Category that produced this event. + public ServusTraceCategory Category { get; } + + /// Short type name of the source object (from GetType().Name). + public string SourceType { get; } + + /// Identity hash of the source object (from GetHashCode()). + public int SourceHash { get; } + + /// Format template (compatible with ). + public string Template { get; } + + private readonly object?[] _args; + + internal ServusTraceEvent( + long timestampTicks, + ServusTraceLevel level, + ServusTraceCategory category, + string sourceType, + int sourceHash, + string template, + params object?[] args) + { + TimestampTicks = timestampTicks; + Level = level; + Category = category; + SourceType = sourceType; + SourceHash = sourceHash; + Template = template; + _args = args; + } + + /// + /// Formats the message by applying stored arguments to the template. + /// This is the only method that allocates a string. + /// + public string FormatMessage() + { + return string.Format(Template, args: _args); + } +} diff --git a/src/Servus.Akka/Diagnostics/ServusTraceExtensions.cs b/src/Servus.Akka/Diagnostics/ServusTraceExtensions.cs new file mode 100644 index 000000000..58df7e356 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusTraceExtensions.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Servus.Akka.Diagnostics; + +/// +/// Extension methods for registering services with +/// . +/// +public static class ServusTraceExtensions +{ + /// + /// Registers a as a singleton + /// and configures + /// when the listener is first resolved. + /// + /// The service collection to add to. + /// Bitwise combination of categories to enable. + /// Minimum trace level to accept. + /// The same for chaining. + public static IServiceCollection AddServusLoggerTracing( + this IServiceCollection services, + ServusTraceCategory categories = ServusTraceCategory.All, + ServusTraceLevel minimumLevel = ServusTraceLevel.Debug) + { + services.AddSingleton(sp => + { + var loggerFactory = sp.GetRequiredService(); + var listener = new LoggerServusTraceListener(loggerFactory, categories, minimumLevel); + ServusTrace.Configure(listener, categories, minimumLevel); + return listener; + }); + return services; + } + + /// + /// Registers a custom as a singleton and + /// configures immediately. + /// + /// The service collection to add to. + /// The custom trace listener to register. + /// Bitwise combination of categories to enable. + /// Minimum trace level to accept. + /// The same for chaining. + public static IServiceCollection AddServusTraceListener( + this IServiceCollection services, + IServusTraceListener listener, + ServusTraceCategory categories = ServusTraceCategory.All, + ServusTraceLevel minimumLevel = ServusTraceLevel.Debug) + { + ArgumentNullException.ThrowIfNull(listener); + ServusTrace.Configure(listener, categories, minimumLevel); + services.AddSingleton(listener); + return services; + } +} diff --git a/src/Servus.Akka/Diagnostics/ServusTraceLevel.cs b/src/Servus.Akka/Diagnostics/ServusTraceLevel.cs new file mode 100644 index 000000000..f80775924 --- /dev/null +++ b/src/Servus.Akka/Diagnostics/ServusTraceLevel.cs @@ -0,0 +1,15 @@ +namespace Servus.Akka.Diagnostics; + +/// +/// Severity levels for events. +/// Values map directly to +/// (Trace=0, Debug=1, Information=2, Warning=3, Error=4). +/// +public enum ServusTraceLevel : byte +{ + Trace = 0, + Debug = 1, + Info = 2, + Warning = 3, + Error = 4, +} diff --git a/src/Servus.Akka/IO/AbruptCloseException.cs b/src/Servus.Akka/IO/AbruptCloseException.cs new file mode 100644 index 000000000..02e11ac2e --- /dev/null +++ b/src/Servus.Akka/IO/AbruptCloseException.cs @@ -0,0 +1,4 @@ +namespace Servus.Akka.IO; + +public sealed class AbruptCloseException() + : Exception("Connection closed abruptly without close_notify"); \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/ClientByteMover.cs b/src/Servus.Akka/IO/ClientByteMover.cs similarity index 53% rename from src/TurboHTTP/Transport/Connection/ClientByteMover.cs rename to src/Servus.Akka/IO/ClientByteMover.cs index c87d5912b..3dd9454e8 100644 --- a/src/TurboHTTP/Transport/Connection/ClientByteMover.cs +++ b/src/Servus.Akka/IO/ClientByteMover.cs @@ -1,25 +1,26 @@ using System.Buffers; -using TurboHTTP.Internal; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO; -internal static class ClientByteMover +public static class ClientByteMover { - /// - /// Threshold below which consecutive small buffers are coalesced into a single write. - /// Reduces syscall overhead for HTTP/2 frame headers (9 bytes) and small DATA frames. - /// + // Threshold below which consecutive small buffers are coalesced into a single write. + // Reduces syscall overhead for HTTP/2 frame headers (9 bytes) and small DATA frames. private const int CoalesceThreshold = 16 * 1024; - /// - /// Reads bytes directly from 's network stream into pooled buffers - /// and writes them to the inbound channel. Eliminates the Pipe intermediary and the - /// associated per-chunk copy. - /// - internal static async Task MoveStreamToChannel(ClientState state, Action onClose, CancellationToken ct, + // Cached delegates — created once at class init, reused for every connection. + // Avoids a delegate heap allocation on each MoveStreamToChannel call. + private static readonly Func DefaultFactory = NetworkBuffer.Rent; + internal static readonly Func Http3Factory = RoutedNetworkBuffer.Rent; + + public static async Task MoveStreamToChannel( + ClientState state, + Action onClose, + CancellationToken ct, Func? bufferFactory = null) { - bufferFactory ??= NetworkBuffer.Rent; + bufferFactory ??= DefaultFactory; + var abrupt = false; try { while (!ct.IsCancellationRequested) @@ -39,7 +40,7 @@ internal static async Task MoveStreamToChannel(ClientState state, Action onClose catch (Exception) { buffer.Dispose(); - state.CloseKind = TlsCloseKind.AbruptClose; + abrupt = true; onClose(); return; } @@ -47,7 +48,6 @@ internal static async Task MoveStreamToChannel(ClientState state, Action onClose if (bytesRead == 0) { buffer.Dispose(); - state.CloseKind = TlsCloseKind.CleanClose; onClose(); return; } @@ -61,7 +61,7 @@ internal static async Task MoveStreamToChannel(ClientState state, Action onClose } finally { - if (state.CloseKind == TlsCloseKind.AbruptClose) + if (abrupt) { state.InboundWriter.TryComplete(new AbruptCloseException()); } @@ -72,11 +72,11 @@ internal static async Task MoveStreamToChannel(ClientState state, Action onClose } } - internal static async Task MoveChannelToStream(ClientState state, Action onClose, CancellationToken ct) + public static async Task MoveChannelToStream(ClientState state, Action onClose, CancellationToken ct) { - // Coalesce buffer lives for the entire connection — avoids ArrayPool rent/return - // per drain cycle. Rented lazily on first small write, returned on exit. - byte[]? coalesceBuf = null; + // Coalesce buffer lives for the entire connection — rented lazily on first small write, + // returned on exit. MemoryPool avoids a raw byte[] heap allocation (ArrayPool is banned). + IMemoryOwner? coalesceOwner = null; try { @@ -86,45 +86,38 @@ internal static async Task MoveChannelToStream(ClientState state, Action onClose { while (await state.OutboundReader.WaitToReadAsync(ct).ConfigureAwait(false)) { - // Drain all available buffers. When multiple small buffers are ready - // (common for HTTP/2 frame headers + small DATA frames), coalesce them - // into a single write to reduce syscall overhead. var coalesceLen = 0; while (state.OutboundReader.TryRead(out var buf)) { try { - var span = buf.Memory; + var mem = buf.Memory; - // If the buffer is large or coalescing would overflow, flush - // the coalesce buffer first, then write the large buffer directly. - if (span.Length > CoalesceThreshold) + if (mem.Length > CoalesceThreshold) { if (coalesceLen > 0) { await state.Stream.WriteAsync( - coalesceBuf.AsMemory(0, coalesceLen), ct).ConfigureAwait(false); + coalesceOwner!.Memory[..coalesceLen], ct).ConfigureAwait(false); coalesceLen = 0; } - await state.Stream.WriteAsync(span, ct).ConfigureAwait(false); + await state.Stream.WriteAsync(mem, ct).ConfigureAwait(false); } else { - // Small buffer — coalesce into a single write. - coalesceBuf ??= ArrayPool.Shared.Rent(CoalesceThreshold); + coalesceOwner ??= MemoryPool.Shared.Rent(CoalesceThreshold); - if (coalesceLen + span.Length > coalesceBuf.Length) + if (coalesceLen + mem.Length > coalesceOwner.Memory.Length) { - // Flush current batch before adding more. await state.Stream.WriteAsync( - coalesceBuf.AsMemory(0, coalesceLen), ct).ConfigureAwait(false); + coalesceOwner.Memory[..coalesceLen], ct).ConfigureAwait(false); coalesceLen = 0; } - span.CopyTo(coalesceBuf.AsMemory(coalesceLen)); - coalesceLen += span.Length; + mem.CopyTo(coalesceOwner.Memory[coalesceLen..]); + coalesceLen += mem.Length; } } finally @@ -133,16 +126,14 @@ await state.Stream.WriteAsync( } } - // Flush remaining coalesced data. if (coalesceLen > 0) { await state.Stream.WriteAsync( - coalesceBuf.AsMemory(0, coalesceLen), ct).ConfigureAwait(false); + coalesceOwner!.Memory[..coalesceLen], ct).ConfigureAwait(false); } - // No FlushAsync needed — Socket.NoDelay = true ensures data is - // sent immediately without Nagle buffering. For SslStream, each - // WriteAsync already emits a self-contained TLS record. + // No FlushAsync needed — Socket.NoDelay = true ensures data is sent immediately. + // For SslStream each WriteAsync already emits a self-contained TLS record. } } catch (OperationCanceledException) @@ -152,7 +143,6 @@ await state.Stream.WriteAsync( } catch (Exception) { - state.CloseKind ??= TlsCloseKind.AbruptClose; onClose(); return; } @@ -160,15 +150,12 @@ await state.Stream.WriteAsync( } finally { - if (coalesceBuf is not null) - { - ArrayPool.Shared.Return(coalesceBuf); - } + coalesceOwner?.Dispose(); } - // Outbound channel was completed without error — signal write-side FIN. + // Outbound channel drained normally — signal write-side FIN. // For QUIC request streams this calls QuicStream.CompleteWrites() so the server // sees end-of-request while the read side stays open for the response. state.OnWritesComplete?.Invoke(); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP/Transport/Connection/ClientState.cs b/src/Servus.Akka/IO/ClientState.cs similarity index 89% rename from src/TurboHTTP/Transport/Connection/ClientState.cs rename to src/Servus.Akka/IO/ClientState.cs index 7733ef7b6..9124f543a 100644 --- a/src/TurboHTTP/Transport/Connection/ClientState.cs +++ b/src/Servus.Akka/IO/ClientState.cs @@ -1,26 +1,19 @@ using System.Threading.Channels; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Quic; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO; -internal sealed class ClientState : IDisposable +public sealed class ClientState : IDisposable { public Stream Stream { get; } public StreamDirection Direction { get; } - /// - /// Indicates how the transport connection was closed. - /// Set by when the read loop exits. - /// - public TlsCloseKind? CloseKind { get; set; } - /// /// Optional callback invoked by /// after the outbound channel is fully drained and completed normally (no error or cancellation). /// Used by QUIC request streams to send FIN on the write side without closing the read side. /// - internal Action? OnWritesComplete { get; init; } + public Action? OnWritesComplete { get; init; } private readonly Channel _inboundChannel; private readonly Channel _outboundChannel; diff --git a/src/TurboHTTP/Transport/Connection/ConnectionHandle.cs b/src/Servus.Akka/IO/ConnectionHandle.cs similarity index 85% rename from src/TurboHTTP/Transport/Connection/ConnectionHandle.cs rename to src/Servus.Akka/IO/ConnectionHandle.cs index b238cee79..00a635a3f 100644 --- a/src/TurboHTTP/Transport/Connection/ConnectionHandle.cs +++ b/src/Servus.Akka/IO/ConnectionHandle.cs @@ -1,14 +1,14 @@ using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO; /// /// Bundles the Channel read/write handles for a single TCP connection, /// allowing ConnectionStage to get direct access to TCP I/O without actor messages. /// -internal sealed record ConnectionHandle( +public sealed record ConnectionHandle( ChannelWriter OutboundWriter, ChannelReader InboundReader, RequestEndpoint Key, @@ -20,8 +20,8 @@ internal sealed record ConnectionHandle( /// /// Indicates how the transport connection was closed. - /// Set by via - /// and read by when the inbound pump completes. + /// Set by via + /// and read by when the inbound pump completes. /// public TlsCloseKind CloseKind { get; private set; } diff --git a/src/TurboHTTP/Transport/Connection/ConnectionLease.cs b/src/Servus.Akka/IO/ConnectionLease.cs similarity index 90% rename from src/TurboHTTP/Transport/Connection/ConnectionLease.cs rename to src/Servus.Akka/IO/ConnectionLease.cs index 408a745f1..cee571a5f 100644 --- a/src/TurboHTTP/Transport/Connection/ConnectionLease.cs +++ b/src/Servus.Akka/IO/ConnectionLease.cs @@ -1,17 +1,16 @@ -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; +using Servus.Akka.Diagnostics; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO; /// /// Wraps a and with lifecycle /// management, metrics emission, and stream tracking. Each lease represents a single /// owner responsible for cleanup when the connection is no longer needed. /// -internal sealed class ConnectionLease : IDisposable +public sealed class ConnectionLease : IDisposable { private readonly CancellationTokenSource _cts = new(); - private readonly long _createdTicks = DateTime.UtcNow.Ticks; + private readonly long _createdTicks = Environment.TickCount64; public ConnectionLease(ConnectionHandle handle, ClientState state) { @@ -82,7 +81,7 @@ public bool IsExpired(TimeSpan maxLifetime) return false; } - return DateTime.UtcNow.Ticks - _createdTicks > (long)maxLifetime.TotalMilliseconds; + return Environment.TickCount64 - _createdTicks > (long)maxLifetime.TotalMilliseconds; } /// @@ -149,12 +148,12 @@ public void Dispose() var host = Key.Host; var port = Key.Port; - TurboHttpMetrics.ConnectionDuration.Record( + ServusTrace.Connection.Debug(this, "Connection to {0}:{1} disposed after {2}ms", host, port, durationMs); + + ServusMetrics.ConnectionDuration.Record( durationMs / 1000.0, new("server.address", host), new("server.port", port)); - TurboHttpEventSource.Instance.ConnectionStop(host, port, durationMs); - TurboTrace.Connection.Info(this, "Connection closed: {0}:{1} ({2}ms)", host, port, durationMs); } private static int ComputeDefaultMaxConcurrentStreams(Version version) diff --git a/src/TurboHTTP/Transport/Connection/IClientProvider.cs b/src/Servus.Akka/IO/IClientProvider.cs similarity index 95% rename from src/TurboHTTP/Transport/Connection/IClientProvider.cs rename to src/Servus.Akka/IO/IClientProvider.cs index 15720d57f..85296f8d4 100644 --- a/src/TurboHTTP/Transport/Connection/IClientProvider.cs +++ b/src/Servus.Akka/IO/IClientProvider.cs @@ -1,12 +1,12 @@ using System.Net; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO; /// /// Abstracts a raw TCP or TLS connection so that is independent /// of the underlying transport. /// -internal interface IClientProvider : IAsyncDisposable +public interface IClientProvider : IAsyncDisposable { /// Gets the remote endpoint the socket is connected to, or if not yet connected. EndPoint? RemoteEndPoint { get; } diff --git a/src/Servus.Akka/IO/IConnectionFactory.cs b/src/Servus.Akka/IO/IConnectionFactory.cs new file mode 100644 index 000000000..d2173693f --- /dev/null +++ b/src/Servus.Akka/IO/IConnectionFactory.cs @@ -0,0 +1,6 @@ +namespace Servus.Akka.IO; + +public interface IConnectionFactory +{ + Task EstablishAsync(ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct); +} diff --git a/src/Servus.Akka/IO/ITransportFactory.cs b/src/Servus.Akka/IO/ITransportFactory.cs new file mode 100644 index 000000000..32b574b1a --- /dev/null +++ b/src/Servus.Akka/IO/ITransportFactory.cs @@ -0,0 +1,9 @@ +using Akka; +using Akka.Streams.Dsl; + +namespace Servus.Akka.IO; + +public interface ITransportFactory +{ + Flow Create(); +} \ No newline at end of file diff --git a/src/Servus.Akka/IO/ITransportOptions.cs b/src/Servus.Akka/IO/ITransportOptions.cs new file mode 100644 index 000000000..f226a63b1 --- /dev/null +++ b/src/Servus.Akka/IO/ITransportOptions.cs @@ -0,0 +1,10 @@ +namespace Servus.Akka.IO; + +public interface ITransportOptions +{ + string Host { get; init; } + int Port { get; init; } + TimeSpan ConnectTimeout { get; init; } + int? SocketSendBufferSize { get; init; } + int? SocketReceiveBufferSize { get; init; } +} \ No newline at end of file diff --git a/src/TurboHTTP/Internal/Messages.cs b/src/Servus.Akka/IO/Messages.cs similarity index 54% rename from src/TurboHTTP/Internal/Messages.cs rename to src/Servus.Akka/IO/Messages.cs index 030c716d5..247d647c9 100644 --- a/src/TurboHTTP/Internal/Messages.cs +++ b/src/Servus.Akka/IO/Messages.cs @@ -1,43 +1,42 @@ using System.Buffers; using System.Collections.Concurrent; -using TurboHTTP.Protocol.Http11; -using TurboHTTP.Transport.Connection; -namespace TurboHTTP.Internal; +namespace Servus.Akka.IO; -internal interface IInputItem +public interface IInputItem { RequestEndpoint Key { get; } } -internal interface IOutputItem +public interface IOutputItem { RequestEndpoint Key { get; } } -internal interface IControlItem : IOutputItem; +public interface IControlItem : IOutputItem; -internal readonly record struct ConnectionReuseItem(ConnectionReuseDecision Decision) : IControlItem +public readonly record struct ConnectionReuseItem(bool CanReuse) : IControlItem { public RequestEndpoint Key { get; init; } } -internal readonly record struct ConnectItem(TcpOptions Options) : IControlItem +public readonly record struct ConnectItem(ITransportOptions Options) : IControlItem { public RequestEndpoint Key { get; init; } + public bool IsReconnect { get; init; } } -internal readonly record struct MaxConcurrentStreamsItem(int MaxStreams) : IControlItem +public readonly record struct MaxConcurrentStreamsItem(int MaxStreams) : IControlItem { public RequestEndpoint Key { get; init; } } -internal readonly record struct StreamAcquireItem : IControlItem +public readonly record struct StreamAcquireItem : IControlItem { public RequestEndpoint Key { get; init; } } -internal enum TlsCloseKind +public enum TlsCloseKind { /// /// The peer sent a TLS close_notify alert before closing the connection, @@ -54,22 +53,17 @@ internal enum TlsCloseKind AbruptClose } -internal readonly record struct CloseSignalItem(TlsCloseKind CloseKind) : IInputItem +public readonly record struct CloseSignalItem(TlsCloseKind CloseKind) : IInputItem { public RequestEndpoint Key { get; init; } } -internal readonly record struct ConnectedSignalItem : IInputItem +public readonly record struct ConnectedSignalItem : IInputItem { public RequestEndpoint Key { get; init; } } -internal readonly record struct ReconnectItem : IControlItem -{ - public RequestEndpoint Key { get; init; } -} - -internal class NetworkBuffer : IInputItem, IOutputItem +public class NetworkBuffer : IInputItem, IOutputItem { private static readonly ConcurrentStack WrapperPool = new(); @@ -87,9 +81,9 @@ internal class NetworkBuffer : IInputItem, IOutputItem public Memory FullMemory => Owner!.Memory; - internal int Capacity => Owner?.Memory.Length ?? 0; + public int Capacity => Owner?.Memory.Length ?? 0; - internal static void ConfigurePoolSize(int maxPoolSize) + public static void ConfigurePoolSize(int maxPoolSize) { MaxPoolSize = maxPoolSize; } @@ -125,44 +119,27 @@ public virtual void Dispose() } } -internal enum Http3StreamType +public class RoutedNetworkBuffer : NetworkBuffer { - None, - - /// Bidirectional request stream (default for request/response data). - Request, + private static readonly ConcurrentStack WrapperPool = new(); - /// Unidirectional control stream (type 0x00) — carries SETTINGS and GOAWAY frames. - Control, - - /// Unidirectional QPACK encoder instruction stream (type 0x02). - QpackEncoder, - - /// Unidirectional QPACK decoder instruction stream (type 0x03). - QpackDecoder, -} - -internal class Http3NetworkBuffer : NetworkBuffer -{ - private static readonly ConcurrentStack WrapperPool = new(); + public long? StreamTypeValue { get; set; } - public Http3StreamType StreamType { get; set; } = Http3StreamType.None; + public long? StreamId { get; set; } - public long StreamId { get; set; } = -1; - - public new static Http3NetworkBuffer Rent(int minimumSize) + public new static RoutedNetworkBuffer Rent(int minimumSize) { var owner = MemoryPool.Shared.Rent(minimumSize); if (!WrapperPool.TryPop(out var buf)) { - return new Http3NetworkBuffer { Owner = owner }; + return new RoutedNetworkBuffer { Owner = owner }; } buf.Owner = owner; buf.Length = 0; buf.Key = default; - buf.StreamType = Http3StreamType.None; - buf.StreamId = -1; + buf.StreamTypeValue = null; + buf.StreamId = null; return buf; } @@ -176,24 +153,13 @@ public override void Dispose() } } -/// -/// Signals that all HTTP/3 frames for the current request have been emitted. -/// The transport handles this by completing the request stream's write side, -/// which causes the QUIC layer to send FIN and lets the server process the request. -/// RFC 9114 §4.1: the client MUST send a FIN on the request stream after the last frame. -/// -internal readonly record struct Http3EndOfRequestItem : IOutputItem +public readonly record struct Http3EndOfRequestItem : IOutputItem { public RequestEndpoint Key { get; init; } public long StreamId { get; init; } } -/// -/// Discriminates the reason a QUIC stream or connection was closed. -/// Used by so the protocol layer can choose -/// the appropriate recovery strategy (flush response, reconnect, or complete). -/// -internal enum QuicCloseKind +public enum QuicCloseKind { /// /// Server sent FIN on the request stream. The response body is delimited @@ -224,14 +190,18 @@ internal enum QuicCloseKind AcquisitionFailed, } -/// -/// Unified close signal for the QUIC transport layer. Consolidates all QUIC -/// close scenarios into a single message type with a -/// discriminator so the protocol stage can choose the appropriate recovery path. -/// The discriminator tells the protocol stage -/// which recovery path to take. -/// -internal readonly record struct QuicCloseItem(QuicCloseKind Kind, long StreamId = -1) : IInputItem +public readonly record struct QuicCloseItem(QuicCloseKind Kind, long StreamId = -1) : IInputItem +{ + public RequestEndpoint Key { get; init; } +} + +public readonly record struct OpenTypedStreamItem(long StreamTypeValue, long SyntheticStreamId, bool Outbound) + : IOutputItem +{ + public RequestEndpoint Key { get; init; } +} + +public readonly record struct ProtocolReadyItem : IOutputItem { public RequestEndpoint Key { get; init; } } \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/IQuicConnectionFactory.cs b/src/Servus.Akka/IO/Quic/IQuicConnectionFactory.cs similarity index 51% rename from src/TurboHTTP/Transport/Connection/IQuicConnectionFactory.cs rename to src/Servus.Akka/IO/Quic/IQuicConnectionFactory.cs index 094ad0a71..1e67599a0 100644 --- a/src/TurboHTTP/Transport/Connection/IQuicConnectionFactory.cs +++ b/src/Servus.Akka/IO/Quic/IQuicConnectionFactory.cs @@ -1,8 +1,6 @@ -using TurboHTTP.Internal; +namespace Servus.Akka.IO.Quic; -namespace TurboHTTP.Transport.Connection; - -internal interface IQuicConnectionFactory +public interface IQuicConnectionFactory { Task EstablishAsync(QuicOptions options, RequestEndpoint endpoint, CancellationToken ct); } diff --git a/src/Servus.Akka/IO/Quic/IQuicTransportEvent.cs b/src/Servus.Akka/IO/Quic/IQuicTransportEvent.cs new file mode 100644 index 000000000..cab7bf6bd --- /dev/null +++ b/src/Servus.Akka/IO/Quic/IQuicTransportEvent.cs @@ -0,0 +1,29 @@ +namespace Servus.Akka.IO.Quic; + +public interface IQuicTransportEvent; + +public readonly record struct ConnectionLeaseAcquired(QuicConnectionLease Lease) : IQuicTransportEvent; + +public readonly record struct RequestLeaseAcquired(ConnectionLease Lease, long StreamId) : IQuicTransportEvent; + +public readonly record struct TypedLeaseAcquired(ConnectionLease Lease, long StreamTypeValue, long StreamId) : IQuicTransportEvent; + +public readonly record struct AcquisitionFailed(Exception Error) : IQuicTransportEvent; + +public readonly record struct InboundData(IInputItem Item, int Gen) : IQuicTransportEvent; + +public readonly record struct InboundComplete(QuicCloseKind CloseKind, int Gen, long StreamId) : IQuicTransportEvent; + +public readonly record struct InboundPumpFailed(Exception Error, long StreamId) : IQuicTransportEvent; + +public readonly record struct InboundStreamReady(QuicConnectionHandle.InboundStream Stream) : IQuicTransportEvent; + +public readonly record struct OutboundWriteDone : IQuicTransportEvent; + +public readonly record struct OutboundWriteFailed(Exception Error) : IQuicTransportEvent; + +public readonly record struct EarlyDataRejected(NetworkBuffer Buffer) : IQuicTransportEvent; + +public readonly record struct ConnectionMigrated( + System.Net.EndPoint? OldLocalEndPoint, + System.Net.EndPoint? NewLocalEndPoint) : IQuicTransportEvent; diff --git a/src/TurboHTTP/Transport/Connection/QuicClientProvider.cs b/src/Servus.Akka/IO/Quic/QuicClientProvider.cs similarity index 97% rename from src/TurboHTTP/Transport/Connection/QuicClientProvider.cs rename to src/Servus.Akka/IO/Quic/QuicClientProvider.cs index 82a800b39..9d5c57b2c 100644 --- a/src/TurboHTTP/Transport/Connection/QuicClientProvider.cs +++ b/src/Servus.Akka/IO/Quic/QuicClientProvider.cs @@ -2,9 +2,8 @@ using System.Net.Quic; using System.Net.Security; using System.Runtime.Versioning; -using TurboHTTP.Transport.Quic; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Quic; /// /// Pure transport QUIC implementation of . Establishes a single QUIC @@ -15,7 +14,7 @@ namespace TurboHTTP.Transport.Connection; [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] [SupportedOSPlatform("windows")] -internal sealed class QuicClientProvider(QuicOptions options) : IClientProvider +public sealed class QuicClientProvider(QuicOptions options) : IClientProvider { private QuicConnection? _connection; private readonly SemaphoreSlim _connectLock = new(1, 1); diff --git a/src/TurboHTTP/Transport/Quic/QuicConnectionFactory.cs b/src/Servus.Akka/IO/Quic/QuicConnectionFactory.cs similarity index 75% rename from src/TurboHTTP/Transport/Quic/QuicConnectionFactory.cs rename to src/Servus.Akka/IO/Quic/QuicConnectionFactory.cs index e7c3c84ec..77f667179 100644 --- a/src/TurboHTTP/Transport/Quic/QuicConnectionFactory.cs +++ b/src/Servus.Akka/IO/Quic/QuicConnectionFactory.cs @@ -1,16 +1,17 @@ -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Tcp; + // QUIC APIs are platform-guarded; usage is gated at runtime via QuicOptions. + +using Servus.Akka.Diagnostics; +using Servus.Akka.IO.Tcp; + #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Eagerly establishes a new QUIC connection and wraps it in a . -/// Mirrors for the QUIC path. +/// Mirrors for the QUIC path. /// internal sealed class QuicConnectionFactory : IQuicConnectionFactory { @@ -30,13 +31,13 @@ public async Task EstablishAsync( var handle = new QuicConnectionHandle(provider, options, endpoint); var lease = new QuicConnectionLease(handle); - TurboHttpMetrics.OpenConnections.Add(1, + ServusTrace.Connection.Debug(Instance, "QUIC connected to {0}:{1}", endpoint.Host, endpoint.Port); + + ServusMetrics.OpenConnections.Add(1, new("http.connection.state", "active"), new("server.address", endpoint.Host), new("server.port", endpoint.Port)); - TurboTrace.Connection.Info(handle, "QUIC connection established: {0}:{1}", endpoint.Host, endpoint.Port); - return lease; } } \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/QuicConnectionHandle.cs b/src/Servus.Akka/IO/Quic/QuicConnectionHandle.cs similarity index 69% rename from src/TurboHTTP/Transport/Connection/QuicConnectionHandle.cs rename to src/Servus.Akka/IO/Quic/QuicConnectionHandle.cs index 18bace88f..ab4dcc0df 100644 --- a/src/TurboHTTP/Transport/Connection/QuicConnectionHandle.cs +++ b/src/Servus.Akka/IO/Quic/QuicConnectionHandle.cs @@ -1,31 +1,20 @@ using System.Runtime.Versioning; -using TurboHTTP.Internal; -using TurboHTTP.Protocol.Http3; -using TurboHTTP.Transport.Quic; // QUIC APIs are platform-guarded; usage is gated at runtime via QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Quic; -/// -/// Wraps a for a single QUIC connection and exposes -/// typed stream-opening and inbound-stream acceptance. -/// -/// Mirrors structurally. Carries the QUIC-specific -/// record (moved from the deleted QuicConnectionManager). -/// -/// [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] [SupportedOSPlatform("windows")] -internal sealed class QuicConnectionHandle : IAsyncDisposable +public sealed class QuicConnectionHandle : IAsyncDisposable { /// /// Notification produced when the inbound-accept loop receives a server-initiated stream. /// Equivalent to the old QuicConnectionManager.InboundStream record. /// - public sealed record InboundStream(ConnectionLease Lease, Http3StreamType StreamType); + public sealed record InboundStream(ConnectionLease Lease, long StreamTypeValue, long StreamId); private readonly IClientProvider _provider; private readonly QuicOptions _options; @@ -49,9 +38,11 @@ public QuicConnectionHandle(IClientProvider provider, QuicOptions options, Reque /// Opens a typed QUIC stream and returns a for it. /// public async Task OpenStreamAsLeaseAsync( - Http3StreamType streamType, CancellationToken ct = default) + bool bidirectional, CancellationToken ct = default) { - var (direction, streamFactory) = MapStreamType(streamType); + var (direction, streamFactory) = bidirectional + ? (StreamDirection.Bidirectional, (Func>)_provider.GetStreamAsync) + : (StreamDirection.WriteOnly, _provider.GetUnidirectionalStreamAsync); var stream = await streamFactory(ct).ConfigureAwait(false); return CreateStreamLease(stream, direction); } @@ -96,28 +87,13 @@ public async Task OpenStreamAsLeaseAsync( return null; } - if (!QuicVarInt.TryDecode(typeBuf.AsSpan(0, bytesRead), out var streamTypeValue, out _)) - { - await stream.DisposeAsync().ConfigureAwait(false); - return null; - } - - var h3StreamType = (StreamType)streamTypeValue switch - { - StreamType.Control => Http3StreamType.Control, - StreamType.QpackEncoder => Http3StreamType.QpackEncoder, - StreamType.QpackDecoder => Http3StreamType.QpackDecoder, - _ => (Http3StreamType?)null, - }; - - if (h3StreamType is null) - { - await stream.DisposeAsync().ConfigureAwait(false); - return null; - } + long streamTypeValue = typeBuf[0]; var lease = CreateStreamLease(stream, StreamDirection.ReadOnly); - return new InboundStream(lease, h3StreamType.Value); + var streamId = stream is System.Net.Quic.QuicStream quicStream + ? quicStream.Id + : -1; + return new InboundStream(lease, streamTypeValue, streamId); } /// @@ -171,7 +147,7 @@ private ConnectionLease CreateStreamLease(Stream stream, StreamDirection directi if (direction != StreamDirection.WriteOnly) { _ = ClientByteMover.MoveStreamToChannel(state, static () => { }, lease.Token, - bufferFactory: Http3NetworkBuffer.Rent); + bufferFactory: ClientByteMover.Http3Factory); } if (direction != StreamDirection.ReadOnly) @@ -181,17 +157,4 @@ private ConnectionLease CreateStreamLease(Stream stream, StreamDirection directi return lease; } - - private (StreamDirection Direction, Func> StreamFactory) - MapStreamType(Http3StreamType streamType) - { - return streamType switch - { - Http3StreamType.Request => (StreamDirection.Bidirectional, _provider.GetStreamAsync), - Http3StreamType.Control => (StreamDirection.WriteOnly, _provider.GetUnidirectionalStreamAsync), - Http3StreamType.QpackEncoder => (StreamDirection.WriteOnly, _provider.GetUnidirectionalStreamAsync), - _ => throw new ArgumentOutOfRangeException(nameof(streamType), streamType, - "Unknown output stream type"), - }; - } } \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/QuicConnectionLease.cs b/src/Servus.Akka/IO/Quic/QuicConnectionLease.cs similarity index 92% rename from src/TurboHTTP/Transport/Connection/QuicConnectionLease.cs rename to src/Servus.Akka/IO/Quic/QuicConnectionLease.cs index baf59784b..e0e923e91 100644 --- a/src/TurboHTTP/Transport/Connection/QuicConnectionLease.cs +++ b/src/Servus.Akka/IO/Quic/QuicConnectionLease.cs @@ -1,8 +1,7 @@ using System.Runtime.Versioning; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; +using Servus.Akka.Diagnostics; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Quic; /// /// Wraps a with lifecycle management, metrics emission, @@ -17,7 +16,7 @@ namespace TurboHTTP.Transport.Connection; [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] [SupportedOSPlatform("windows")] -internal sealed class QuicConnectionLease : IDisposable +public sealed class QuicConnectionLease : IDisposable { private readonly long _createdTicks = Environment.TickCount64; @@ -115,10 +114,9 @@ public void Dispose() var host = Key.Host; var port = Key.Port; - TurboHttpMetrics.ConnectionDuration.Record( + ServusMetrics.ConnectionDuration.Record( durationMs / 1000.0, new("server.address", host), new("server.port", port)); - TurboTrace.Connection.Info(this, "QUIC connection closed: {0}:{1} ({2}ms)", host, port, durationMs); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/QuicConnectionManagerActor.cs b/src/Servus.Akka/IO/Quic/QuicConnectionManagerActor.cs similarity index 94% rename from src/TurboHTTP/Transport/Connection/QuicConnectionManagerActor.cs rename to src/Servus.Akka/IO/Quic/QuicConnectionManagerActor.cs index 399379b8f..edf26c28f 100644 --- a/src/TurboHTTP/Transport/Connection/QuicConnectionManagerActor.cs +++ b/src/Servus.Akka/IO/Quic/QuicConnectionManagerActor.cs @@ -1,17 +1,16 @@ using System.Runtime.Versioning; using Akka.Actor; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Quic; +using Servus.Akka.Diagnostics; +using Servus.Akka.IO.Tcp; // QUIC APIs are platform-guarded; usage is gated at runtime via QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Quic; /// /// Single actor that manages ALL per-host QUIC connection state: acquire, release, idle reuse, -/// eviction, and per-host connection limits. Every +/// eviction, and per-host connection limits. Every /// talks to this actor via / . /// /// Per-host state (leases, pending queue, establishing count) is kept in a @@ -27,15 +26,15 @@ namespace TurboHTTP.Transport.Connection; [SupportedOSPlatform("linux")] [SupportedOSPlatform("macOS")] [SupportedOSPlatform("windows")] -internal sealed class QuicConnectionManagerActor : ReceiveActor, IWithTimers +public sealed class QuicConnectionManagerActor : ReceiveActor, IWithTimers { - private sealed record Acquire( + public sealed record Acquire( QuicOptions Options, RequestEndpoint Endpoint, TaskCompletionSource Tcs, CancellationToken Token); - internal sealed record Release(QuicConnectionLease Lease, bool CanReuse); + public sealed record Release(QuicConnectionLease Lease, bool CanReuse); private sealed record Established(QuicConnectionLease Lease, Acquire Original); @@ -142,7 +141,7 @@ private void OnAcquire(Acquire msg) } else { - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "idle"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -191,7 +190,7 @@ private void OnRelease(Release msg) { host.Leases.Remove(msg.Lease); msg.Lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -226,7 +225,7 @@ private void OnEstablished(Established msg) host.Establishing--; host.Leases.Add(msg.Lease); msg.Lease.MarkBusy(); - TurboHttpMetrics.OpenConnections.Add(1, + ServusMetrics.OpenConnections.Add(1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -322,7 +321,7 @@ private void EvictHost(HostState host) foreach (var lease in toEvict) { lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); diff --git a/src/TurboHTTP/Transport/Quic/QuicConnectionStage.cs b/src/Servus.Akka/IO/Quic/QuicConnectionStage.cs similarity index 85% rename from src/TurboHTTP/Transport/Quic/QuicConnectionStage.cs rename to src/Servus.Akka/IO/Quic/QuicConnectionStage.cs index edb9ec085..2fe45e3f2 100644 --- a/src/TurboHTTP/Transport/Quic/QuicConnectionStage.cs +++ b/src/Servus.Akka/IO/Quic/QuicConnectionStage.cs @@ -2,34 +2,31 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO.Tcp; // QUIC APIs are platform-guarded; usage is gated at runtime via ConnectItem.Options being QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Transport stage for HTTP/3 (QUIC). Manages multi-stream I/O (request, control, encoder), /// tagged item routing, and multiple inbound pumps. Connection lifecycle (pooling, reuse, -/// eviction) is handled by . +/// eviction) is handled by . /// -internal sealed class QuicConnectionStage : GraphStage> +public sealed class QuicConnectionStage : GraphStage> { private readonly Inlet _in = new("QuicConnection.In"); private readonly Outlet _out = new("QuicConnection.Out"); private readonly IActorRef _connectionManager; - private readonly TurboClientOptions _clientOptions; private readonly bool _allowConnectionMigration; public override FlowShape Shape { get; } - public QuicConnectionStage(IActorRef connectionManager, TurboClientOptions clientOptions, bool allowConnectionMigration = true) + public QuicConnectionStage(IActorRef connectionManager, bool allowConnectionMigration = true) { _connectionManager = connectionManager; - _clientOptions = clientOptions; _allowConnectionMigration = allowConnectionMigration; Shape = new FlowShape(_in, _out); } @@ -46,7 +43,7 @@ private sealed class Logic : TimerGraphStageLogic, ITransportOperations public Logic(QuicConnectionStage stage) : base(stage.Shape) { _stage = stage; - + SetHandler(stage._in, onPush: () => _sm.HandlePush(Grab(stage._in)), onUpstreamFinish: () => _sm.HandleUpstreamFinish()); @@ -70,7 +67,7 @@ public override void PreStart() { var stageActor = GetStageActor(OnReceive); _sm = new QuicTransportStateMachine(this, stageActor.Ref, _stage._connectionManager, - _stage._clientOptions, _stage._allowConnectionMigration); + _stage._allowConnectionMigration); Pull(_stage._in); } @@ -115,4 +112,4 @@ void ITransportOperations.OnSignalPullInput() ILoggingAdapter ITransportOperations.Log => Log; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/QuicOptions.cs b/src/Servus.Akka/IO/Quic/QuicOptions.cs similarity index 93% rename from src/TurboHTTP/Transport/Connection/QuicOptions.cs rename to src/Servus.Akka/IO/Quic/QuicOptions.cs index 09c2ed083..702caeaeb 100644 --- a/src/TurboHTTP/Transport/Connection/QuicOptions.cs +++ b/src/Servus.Akka/IO/Quic/QuicOptions.cs @@ -1,9 +1,11 @@ -namespace TurboHTTP.Transport.Connection; +using Servus.Akka.IO.Tcp; + +namespace Servus.Akka.IO.Quic; /// /// QUIC connection options, extending with QUIC-specific settings. /// -internal record QuicOptions : TlsOptions +public record QuicOptions : TlsOptions { /// The idle timeout after which the QUIC connection is closed. public TimeSpan IdleTimeout { get; init; } = TimeSpan.FromSeconds(30); diff --git a/src/TurboHTTP/Transport/Quic/QuicPumpManager.cs b/src/Servus.Akka/IO/Quic/QuicPumpManager.cs similarity index 70% rename from src/TurboHTTP/Transport/Quic/QuicPumpManager.cs rename to src/Servus.Akka/IO/Quic/QuicPumpManager.cs index 215b4899e..f88b75e1d 100644 --- a/src/TurboHTTP/Transport/Quic/QuicPumpManager.cs +++ b/src/Servus.Akka/IO/Quic/QuicPumpManager.cs @@ -1,22 +1,20 @@ using System.Threading.Channels; using Akka.Actor; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; // QUIC APIs are platform-guarded; usage is gated at runtime via ConnectItem.Options being QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Manages the lifecycle of QUIC inbound stream pumps — start, cancel, and the async read loops /// that marshal data from QUIC streams into StageActorRef messages. /// Extracted from for single-responsibility. /// -internal sealed class QuicPumpManager +public sealed class QuicPumpManager { private readonly IActorRef _self; - private readonly List _pumpCancellations = []; + private CancellationTokenSource? _pumpsCts; private CancellationTokenSource? _inboundAcceptCts; public QuicPumpManager(IActorRef self) @@ -28,13 +26,11 @@ public QuicPumpManager(IActorRef self) /// Starts a background pump that reads from the given handle's inbound channel /// and marshals each chunk as a message. /// - public void StartInboundPump(ConnectionHandle handle, Http3StreamType streamType, - RequestEndpoint key, int connectionGen, long streamId = -1) + public void StartInboundPump(ConnectionHandle handle, long streamTypeValue, + RequestEndpoint key, int connectionGen, long streamId) { - var cts = new CancellationTokenSource(); - _pumpCancellations.Add(cts); - - _ = PumpAsync(handle.InboundReader, key, streamType, cts.Token, _self, connectionGen, streamId); + _pumpsCts ??= new CancellationTokenSource(); + _ = PumpAsync(handle.InboundReader, key, streamTypeValue, _pumpsCts.Token, _self, connectionGen, streamId); } /// @@ -58,13 +54,9 @@ public void StopAll() _inboundAcceptCts?.Dispose(); _inboundAcceptCts = null; - foreach (var cts in _pumpCancellations) - { - cts.Cancel(); - cts.Dispose(); - } - - _pumpCancellations.Clear(); + _pumpsCts?.Cancel(); + _pumpsCts?.Dispose(); + _pumpsCts = null; } private static async Task AcceptLoopAsync(QuicConnectionHandle handle, IActorRef self, @@ -92,13 +84,13 @@ private static async Task AcceptLoopAsync(QuicConnectionHandle handle, IActorRef private static async Task PumpAsync( ChannelReader reader, RequestEndpoint key, - Http3StreamType streamType, + long streamTypeValue, CancellationToken ct, IActorRef self, int gen, - long streamId = -1) + long streamId) { - var closeKind = TlsCloseKind.CleanClose; + var closeKind = QuicCloseKind.RequestStreamComplete; try { while (await reader.WaitToReadAsync(ct).ConfigureAwait(false)) @@ -107,13 +99,10 @@ private static async Task PumpAsync( { chunk.Key = key; - if (chunk is Http3NetworkBuffer h3Buf) + if (chunk is RoutedNetworkBuffer h3Buf) { - h3Buf.StreamType = streamType; - if (streamType == Http3StreamType.Request) - { - h3Buf.StreamId = streamId; - } + h3Buf.StreamTypeValue = streamTypeValue; + h3Buf.StreamId = streamId; } self.Tell(new InboundData(chunk, gen)); @@ -126,11 +115,11 @@ private static async Task PumpAsync( } catch (AbruptCloseException) { - closeKind = TlsCloseKind.AbruptClose; + closeKind = QuicCloseKind.ConnectionFailure; } catch (ChannelClosedException ex) when (ex.InnerException is AbruptCloseException) { - closeKind = TlsCloseKind.AbruptClose; + closeKind = QuicCloseKind.ConnectionFailure; } catch (Exception ex) { @@ -138,8 +127,7 @@ private static async Task PumpAsync( return; } - // Only emit close signal for the request stream (per-stream lifecycle) - if (streamType == Http3StreamType.Request) + if (streamTypeValue < 0) { self.Tell(new InboundComplete(closeKind, gen, streamId)); } diff --git a/src/TurboHTTP/Transport/Quic/QuicStreamRouter.cs b/src/Servus.Akka/IO/Quic/QuicStreamRouter.cs similarity index 83% rename from src/TurboHTTP/Transport/Quic/QuicStreamRouter.cs rename to src/Servus.Akka/IO/Quic/QuicStreamRouter.cs index eb3a2c350..063c7facf 100644 --- a/src/TurboHTTP/Transport/Quic/QuicStreamRouter.cs +++ b/src/Servus.Akka/IO/Quic/QuicStreamRouter.cs @@ -1,20 +1,18 @@ using Akka.Actor; using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO.Tcp; // QUIC APIs are platform-guarded; usage is gated at runtime via ConnectItem.Options being QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Manages per-stream transport context for concurrent QUIC request streams — /// context creation, tagged item routing, pending write buffering, and flush. /// Extracted from for single-responsibility. /// -internal sealed class QuicStreamRouter +public sealed class QuicStreamRouter { private readonly ITransportOperations _ops; private readonly IActorRef _self; @@ -42,45 +40,46 @@ public QuicStreamRouter(ITransportOperations ops, IActorRef self) /// connection must be established (no existing connection with control stream). /// Returns false if the context already existed or was handled. /// - public StreamContextResult EnsureStreamContext(IOutputItem item, long streamId, + public StreamContextResult EnsureStreamContext(IOutputItem item, long? streamId, bool hasConnection) { - if (streamId < 0 || _requestStreams.ContainsKey(streamId) || string.IsNullOrEmpty(item.Key.Scheme) || + if (streamId is null || streamId.Value < 0 || _requestStreams.ContainsKey(streamId.Value) || + string.IsNullOrEmpty(item.Key.Scheme) || item.Key == RequestEndpoint.Default) { return StreamContextResult.AlreadyExists; } - _requestStreams[streamId] = new RequestStreamContext(); + _requestStreams[streamId.Value] = new RequestStreamContext(); if (hasConnection) { return StreamContextResult.OpenNewStream; } - _pendingOpenStreamIds.Enqueue(streamId); + _pendingOpenStreamIds.Enqueue(streamId.Value); return StreamContextResult.NeedsConnection; } /// /// Routes a tagged item to the appropriate stream (request, control, or encoder). /// - public void RouteTaggedItem(Http3NetworkBuffer dataItem, - ConnectionHandle? controlHandle, Queue pendingControlItems, - ConnectionHandle? encoderHandle, Queue pendingEncoderItems) + public void RouteTaggedItem(RoutedNetworkBuffer dataItem, long? streamTypeValue, + Dictionary typedStreams) { - switch (dataItem.StreamType) + if (streamTypeValue is null) { - case Http3StreamType.Request: - RouteToRequestStream(dataItem.StreamId, dataItem); - break; - case Http3StreamType.Control: - RouteToTypedStream(controlHandle, pendingControlItems, dataItem); - break; - case Http3StreamType.QpackEncoder: - RouteToTypedStream(encoderHandle, pendingEncoderItems, dataItem); - break; + RouteToRequestStream(dataItem.StreamId, dataItem); + return; + } + + if (typedStreams.TryGetValue(streamTypeValue.Value, out var state)) + { + RouteToTypedStream(state.Handle, state.PendingItems, dataItem, state.StreamId); + return; } + + RouteToRequestStream(dataItem.StreamId, dataItem); } /// @@ -234,9 +233,9 @@ public void DisposePendingWrites() } } - private void RouteToRequestStream(long streamId, NetworkBuffer dataItem) + private void RouteToRequestStream(long? streamId, NetworkBuffer dataItem) { - if (streamId >= 0 && _requestStreams.TryGetValue(streamId, out var ctx)) + if (streamId is not null && _requestStreams.TryGetValue(streamId.Value, out var ctx)) { if (ctx.Handle is not null) { @@ -255,10 +254,15 @@ private void RouteToRequestStream(long streamId, NetworkBuffer dataItem) } private void RouteToTypedStream(ConnectionHandle? handle, Queue pendingQueue, - NetworkBuffer dataItem) + NetworkBuffer dataItem, long streamId) { if (handle is not null) { + if (dataItem is RoutedNetworkBuffer h3) + { + h3.StreamId = streamId; + } + WriteToHandle(handle, dataItem); } else @@ -279,22 +283,22 @@ private void WriteToHandle(ConnectionHandle? handle, NetworkBuffer buffer) _ = handle.OutboundWriter.WriteAsync(buffer) .PipeTo(_self, - success: () => new OutboundWriteDone(), - failure: ex => new OutboundWriteFailed(ex.GetBaseException())); + success: static () => new OutboundWriteDone(), + failure: static ex => new OutboundWriteFailed(ex.GetBaseException())); } /// /// Per-stream transport state: tracks the handle, pending writes, and end-of-request flag /// for each concurrent request stream on the QUIC connection. /// - internal sealed class RequestStreamContext + public sealed class RequestStreamContext { public ConnectionHandle? Handle; public readonly Queue PendingWrites = new(); public bool PendingEndOfRequest; } - internal enum StreamContextResult + public enum StreamContextResult { AlreadyExists, OpenNewStream, diff --git a/src/TurboHTTP/Transport/Quic/QuicTransportFactory.cs b/src/Servus.Akka/IO/Quic/QuicTransportFactory.cs similarity index 54% rename from src/TurboHTTP/Transport/Quic/QuicTransportFactory.cs rename to src/Servus.Akka/IO/Quic/QuicTransportFactory.cs index 5a1e81831..dcce8b8f3 100644 --- a/src/TurboHTTP/Transport/Quic/QuicTransportFactory.cs +++ b/src/Servus.Akka/IO/Quic/QuicTransportFactory.cs @@ -1,26 +1,24 @@ using Akka; using Akka.Actor; using Akka.Streams.Dsl; -using TurboHTTP.Internal; -using TurboHTTP.Streams; +using Servus.Akka.IO.Tcp; #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Transport factory for QUIC connections (HTTP/3). -/// Mirrors — accepts a shared -/// pointing to a . +/// Mirrors — accepts a shared +/// pointing to a . /// -internal sealed class QuicTransportFactory( +public sealed class QuicTransportFactory( IActorRef connectionManager, - TurboClientOptions clientOptions, bool allowConnectionMigration = true) : ITransportFactory { /// /// Creates a QUIC transport stage wired to the shared connection manager actor. /// public Flow Create() - => Flow.FromGraph(new QuicConnectionStage(connectionManager, clientOptions, allowConnectionMigration)); + => Flow.FromGraph(new QuicConnectionStage(connectionManager, allowConnectionMigration)); } diff --git a/src/TurboHTTP/Transport/Quic/QuicTransportStateMachine.cs b/src/Servus.Akka/IO/Quic/QuicTransportStateMachine.cs similarity index 67% rename from src/TurboHTTP/Transport/Quic/QuicTransportStateMachine.cs rename to src/Servus.Akka/IO/Quic/QuicTransportStateMachine.cs index 4c1d6701e..a1e96e800 100644 --- a/src/TurboHTTP/Transport/Quic/QuicTransportStateMachine.cs +++ b/src/Servus.Akka/IO/Quic/QuicTransportStateMachine.cs @@ -1,13 +1,11 @@ using Akka.Actor; using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO.Tcp; // QUIC APIs are platform-guarded; usage is gated at runtime via ConnectItem.Options being QuicOptions. #pragma warning disable CA1416 -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Encapsulates all QUIC transport state and logic — multi-stream I/O (request, control, encoder), @@ -24,14 +22,14 @@ namespace TurboHTTP.Transport.Quic; /// pump lifecycle by . /// /// -internal sealed class QuicTransportStateMachine +public sealed class QuicTransportStateMachine { private const string ConnectTimerKey = "connect-timeout"; + private const long RequestStreamTypeValue = -1; private readonly ITransportOperations _ops; private readonly IActorRef _self; private readonly IActorRef _quicManagerActor; - private readonly TurboClientOptions _clientOptions; private readonly bool _allowConnectionMigration; private readonly QuicStreamRouter _router; @@ -40,17 +38,12 @@ internal sealed class QuicTransportStateMachine private int _connectionGen; private QuicConnectionLease? _currentConnectionLease; - private ConnectionHandle? _controlHandle; - private ConnectionHandle? _encoderHandle; - private TlsCloseKind _lastCloseKind = TlsCloseKind.CleanClose; - private bool _needsReconnectSignal; - - /// Pending control items buffered before control stream is ready. - private readonly Queue _pendingControlItems = new(); + private readonly Dictionary _typedStreams = new(); - /// Pending QPACK encoder items buffered before encoder stream is ready. - private readonly Queue _pendingEncoderItems = new(); + private QuicCloseKind _lastCloseKind = QuicCloseKind.RequestStreamComplete; + private bool _needsReconnectSignal; + private bool _protocolReady; /// All active stream leases for this connection (disposed on Cleanup). private readonly List _activeLeases = []; @@ -63,12 +56,11 @@ internal sealed class QuicTransportStateMachine private System.Net.EndPoint? _lastLocalEndPoint; public QuicTransportStateMachine(ITransportOperations ops, IActorRef self, IActorRef quicManagerActor, - TurboClientOptions clientOptions, bool allowConnectionMigration = true) + bool allowConnectionMigration = true) { _ops = ops; _self = self; _quicManagerActor = quicManagerActor; - _clientOptions = clientOptions; _allowConnectionMigration = allowConnectionMigration; _router = new QuicStreamRouter(ops, self); _pumpManager = new QuicPumpManager(self); @@ -85,7 +77,7 @@ public void Dispatch(IQuicTransportEvent evt) OnRequestLeaseAcquired(e.Lease, e.StreamId); break; case TypedLeaseAcquired e: - OnTypedLeaseAcquired(e.Lease, e.StreamType); + OnTypedLeaseAcquired(e.Lease, e.StreamTypeValue, e.StreamId); break; case AcquisitionFailed e: OnAcquisitionFailed(e.Error); @@ -107,7 +99,7 @@ public void Dispatch(IQuicTransportEvent evt) break; case InboundPumpFailed e: _ops.Log.Warning("QuicConnectionStage: Inbound pump failed — {0}", e.Error.Message); - OnInboundComplete(TlsCloseKind.AbruptClose, e.StreamId); + OnInboundComplete(QuicCloseKind.ConnectionFailure, e.StreamId); break; case InboundStreamReady e: OnInboundStreamReady(e.Stream); @@ -129,30 +121,51 @@ public void Dispatch(IQuicTransportEvent evt) public void HandlePush(IOutputItem item) { + switch (item) + { + case OpenTypedStreamItem openItem: + _typedStreams[openItem.StreamTypeValue] = new TypedStreamState + { + StreamId = openItem.SyntheticStreamId, + OriginalSyntheticStreamId = openItem.SyntheticStreamId, + IsOutbound = openItem.Outbound + }; + return; + + case ProtocolReadyItem: + _protocolReady = true; + if (_currentConnectionLease is not null) + { + foreach (var (typeValue, state) in _typedStreams) + { + if (state.IsOutbound) + { + OpenTypedStream(typeValue, state.StreamId); + } + } + + _pumpManager.StartInboundAcceptLoop(_currentConnectionLease.Handle); + } + + return; + } + var streamId = item switch { - Http3NetworkBuffer t => t.StreamId, + RoutedNetworkBuffer t => t.StreamId, Http3EndOfRequestItem e => e.StreamId, - _ => -1L + _ => null }; var result = _router.EnsureStreamContext(item, streamId, - hasConnection: _currentConnectionLease is not null && _controlHandle is not null); + hasConnection: _currentConnectionLease is not null && AllOutboundTypedStreamsReady); switch (result) { case QuicStreamRouter.StreamContextResult.OpenNewStream: - OpenNewRequestStream(streamId); + OpenNewRequestStream(streamId!.Value); break; case QuicStreamRouter.StreamContextResult.NeedsConnection: - // Only start a new connection if one isn't already being acquired or established. - // The stream context was already created — its items will be buffered in pending writes - // and the stream will be opened once the connection is fully ready. - if (_pendingConnect is null && _currentConnectionLease is null) - { - AutoConnect(item.Key); - } - break; } @@ -162,9 +175,15 @@ public void HandlePush(IOutputItem item) HandleConnectItem(connect); break; - case Http3NetworkBuffer tagged when tagged.StreamType != Http3StreamType.None: - _router.RouteTaggedItem(tagged, _controlHandle, _pendingControlItems, - _encoderHandle, _pendingEncoderItems); + case RoutedNetworkBuffer tagged: + var typeValue = tagged.StreamTypeValue; + if (typeValue is null && tagged.StreamId is null) + { + throw new InvalidOperationException( + "QuicConnectionStage: Request stream write requires an explicit non-negative StreamId."); + } + + _router.RouteTaggedItem(tagged, typeValue, _typedStreams); break; case NetworkBuffer dataItem: @@ -172,18 +191,44 @@ public void HandlePush(IOutputItem item) break; case Http3EndOfRequestItem endItem: + if (endItem.StreamId < 0) + { + throw new InvalidOperationException( + "QuicConnectionStage: End-of-request requires an explicit non-negative StreamId."); + } + _router.HandleEndOfRequest(endItem); break; case ConnectionReuseItem: case StreamAcquireItem: case MaxConcurrentStreamsItem: - // QUIC manages these internally — no-op _ops.OnSignalPullInput(); break; } } + private bool AllOutboundTypedStreamsReady + { + get + { + if (!_protocolReady) + { + return false; + } + + foreach (var state in _typedStreams.Values) + { + if (state.IsOutbound && state.Handle is null) + { + return false; + } + } + + return true; + } + } + public void HandleUpstreamFinish() { _pumpManager.StopAll(); @@ -230,32 +275,16 @@ private void HandleConnectItem(ConnectItem connect) _ops.Log.Debug("QuicConnectionStage: ConnectItem key={0}:{1}", connect.Key.Host, connect.Key.Port); CleanupTransport(); - _pendingConnect = connect; - - if (connect.Options is not QuicOptions quicOptions) - { - _self.Tell(new AcquisitionFailed(new InvalidOperationException( - "QuicConnectionStage received a non-QuicOptions ConnectItem."))); - return; - } - - AcquireQuicConnection(quicOptions, connect); - } - - private void AutoConnect(RequestEndpoint endpoint) - { - _ops.Log.Debug("QuicConnectionStage: AutoConnect for {0}:{1}", endpoint.Host, endpoint.Port); - - var options = OptionsFactory.Build(endpoint, _clientOptions); - _pendingConnect = new ConnectItem(options) { Key = endpoint }; + var options = connect.Options!; if (options is not QuicOptions quicOptions) { _self.Tell(new AcquisitionFailed(new InvalidOperationException( - "QuicConnectionStage: AutoConnect produced non-QuicOptions for endpoint."))); + "QuicConnectionStage received a non-QuicOptions ConnectItem."))); return; } + _pendingConnect = connect with { Options = options }; AcquireQuicConnection(quicOptions, _pendingConnect.Value); } @@ -269,7 +298,7 @@ private void OnConnectionLeaseAcquired(QuicConnectionLease lease) return; } - _ = lease.Handle.OpenStreamAsLeaseAsync(Http3StreamType.Request) + _ = lease.Handle.OpenStreamAsLeaseAsync(bidirectional: true) .PipeTo(_self, success: streamLease => new RequestLeaseAcquired(streamLease, streamId), failure: ex => new AcquisitionFailed(ex.GetBaseException())); @@ -286,45 +315,51 @@ private void OnRequestLeaseAcquired(ConnectionLease lease, long streamId) var ctx = _router.GetOrCreateContext(streamId); ctx.Handle = lease.Handle; - _pumpManager.StartInboundPump(lease.Handle, Http3StreamType.Request, _currentKey, _connectionGen, streamId); + _pumpManager.StartInboundPump(lease.Handle, RequestStreamTypeValue, _currentKey, _connectionGen, streamId); - if (_controlHandle is not null) + if (AllOutboundTypedStreamsReady) { _router.FlushPendingWrites(ctx); _ops.OnSignalPullInput(); } - else + else if (_protocolReady) { - OpenTypedStream(Http3StreamType.Control); - OpenTypedStream(Http3StreamType.QpackEncoder); + foreach (var (typeValue, state) in _typedStreams) + { + if (state.IsOutbound) + { + OpenTypedStream(typeValue, state.StreamId); + } + } + _pumpManager.StartInboundAcceptLoop(_currentConnectionLease!.Handle); } } - private void OnTypedLeaseAcquired(ConnectionLease lease, Http3StreamType streamType) + private void OnTypedLeaseAcquired(ConnectionLease lease, long streamTypeValue, long streamId) { _activeLeases.Add(lease); - switch (streamType) + if (!_typedStreams.TryGetValue(streamTypeValue, out var state)) { - case Http3StreamType.Control: - _controlHandle = lease.Handle; - FlushPendingQuicItems(_pendingControlItems, lease.Handle); - _router.FlushAllReadyStreams(); - OpenPendingStreams(); - if (_needsReconnectSignal) - { - _needsReconnectSignal = false; - _ops.OnPushOutput(new ConnectedSignalItem { Key = _currentKey }); - } + return; + } - _ops.OnSignalPullInput(); - break; + state.Handle = lease.Handle; + state.StreamId = streamId; + FlushPendingQuicItems(state.PendingItems, lease.Handle, streamId); - case Http3StreamType.QpackEncoder: - _encoderHandle = lease.Handle; - FlushPendingQuicItems(_pendingEncoderItems, lease.Handle); - break; + if (AllOutboundTypedStreamsReady) + { + _router.FlushAllReadyStreams(); + OpenPendingStreams(); + if (_needsReconnectSignal) + { + _needsReconnectSignal = false; + _ops.OnPushOutput(new ConnectedSignalItem { Key = _currentKey }); + } + + _ops.OnSignalPullInput(); } } @@ -348,8 +383,7 @@ private void OnConnectionMigrated(System.Net.EndPoint? oldEndPoint, System.Net.E _ops.OnPushOutput(signal); _router.Clear(); - _controlHandle = null; - _encoderHandle = null; + ResetTypedStreams(); } private void CheckForConnectionMigration() @@ -384,8 +418,7 @@ private void OnOutboundWriteFailed(Exception ex) _ops.OnPushOutput(signal); _router.Clear(); - _controlHandle = null; - _encoderHandle = null; + ResetTypedStreams(); } private void OnAcquisitionFailed(Exception ex) @@ -406,11 +439,11 @@ private void OnAcquisitionFailed(Exception ex) _ops.OnSignalPullInput(); } - private void OnInboundComplete(TlsCloseKind closeKind, long streamId = -1) + private void OnInboundComplete(QuicCloseKind kind, long streamId) { - _lastCloseKind = closeKind; + _lastCloseKind = kind; - if (closeKind == TlsCloseKind.CleanClose) + if (kind == QuicCloseKind.RequestStreamComplete) { _ops.OnPushOutput(new QuicCloseItem(QuicCloseKind.RequestStreamComplete, streamId) { Key = _currentKey }); _router.RemoveStream(streamId); @@ -418,17 +451,21 @@ private void OnInboundComplete(TlsCloseKind closeKind, long streamId = -1) else { _needsReconnectSignal = true; - _ops.OnPushOutput(new QuicCloseItem(QuicCloseKind.ConnectionFailure) { Key = _currentKey }); + _ops.OnPushOutput(new QuicCloseItem(kind) { Key = _currentKey }); _router.Clear(); - _controlHandle = null; - _encoderHandle = null; + ResetTypedStreams(); } } private void OnInboundStreamReady(QuicConnectionHandle.InboundStream inbound) { _activeLeases.Add(inbound.Lease); - _pumpManager.StartInboundPump(inbound.Lease.Handle, inbound.StreamType, _currentKey, _connectionGen); + var streamId = inbound.StreamId >= 0 + ? inbound.StreamId + : LookupSyntheticStreamId(inbound.StreamTypeValue); + + _pumpManager.StartInboundPump(inbound.Lease.Handle, inbound.StreamTypeValue, _currentKey, _connectionGen, + streamId); } private void AcquireQuicConnection(QuicOptions options, ConnectItem connect) @@ -441,10 +478,10 @@ private void AcquireQuicConnection(QuicOptions options, ConnectItem connect) _quicManagerActor, options, connect.Key, _acquireCts.Token); acquireTask.PipeTo(_self, - success: connLease => new ConnectionLeaseAcquired(connLease), - failure: ex => new AcquisitionFailed(ex.GetBaseException())); + success: static connLease => new ConnectionLeaseAcquired(connLease), + failure: static ex => new AcquisitionFailed(ex.GetBaseException())); - var timeout = connect.Options.ConnectTimeout; + var timeout = options.ConnectTimeout; if (timeout <= TimeSpan.Zero) { timeout = TimeSpan.FromSeconds(10); @@ -455,8 +492,8 @@ private void AcquireQuicConnection(QuicOptions options, ConnectItem connect) private void OpenPendingStreams() { - var pending = _router.DrainPendingStreamIds(); - foreach (var id in pending) + long id; + while ((id = _router.DequeueNextPendingStreamId()) >= 0) { OpenNewRequestStream(id); } @@ -469,26 +506,26 @@ private void OpenNewRequestStream(long streamId) return; } - _ = _currentConnectionLease.Handle.OpenStreamAsLeaseAsync(Http3StreamType.Request) + _ = _currentConnectionLease.Handle.OpenStreamAsLeaseAsync(bidirectional: true) .PipeTo(_self, success: streamLease => new RequestLeaseAcquired(streamLease, streamId), - failure: ex => new AcquisitionFailed(ex.GetBaseException())); + failure: static ex => new AcquisitionFailed(ex.GetBaseException())); } - private void OpenTypedStream(Http3StreamType streamType) + private void OpenTypedStream(long streamTypeValue, long syntheticStreamId) { if (_currentConnectionLease is null) { return; } - _ = _currentConnectionLease.Handle.OpenStreamAsLeaseAsync(streamType) + _ = _currentConnectionLease.Handle.OpenStreamAsLeaseAsync(bidirectional: false) .PipeTo(_self, - success: lease => new TypedLeaseAcquired(lease, streamType), + success: lease => new TypedLeaseAcquired(lease, streamTypeValue, syntheticStreamId), failure: ex => { - _ops.Log.Warning("QuicConnectionStage: Failed to open {0} stream — {1}", - streamType, ex.GetBaseException().Message); + _ops.Log.Warning("QuicConnectionStage: Failed to open typed stream (type=0x{0:X2}) — {1}", + streamTypeValue, ex.GetBaseException().Message); return new AcquisitionFailed(ex.GetBaseException()); }); } @@ -522,24 +559,51 @@ private void CleanupTransport() _activeLeases.Clear(); - ReturnConnectionToPool(_lastCloseKind == TlsCloseKind.CleanClose); - _lastCloseKind = TlsCloseKind.CleanClose; + ReturnConnectionToPool(_lastCloseKind == QuicCloseKind.RequestStreamComplete); + _lastCloseKind = QuicCloseKind.RequestStreamComplete; _router.Clear(); - _controlHandle = null; - _encoderHandle = null; + ResetTypedStreams(); + } + + private void ResetTypedStreams() + { + foreach (var state in _typedStreams.Values) + { + state.Handle = null; + state.StreamId = state.OriginalSyntheticStreamId; + while (state.PendingItems.TryDequeue(out var orphan)) + { + orphan.Dispose(); + } + } + + // _protocolReady is preserved — Http30ConnectionStage emits ProtocolReadyItem once at PreStart. + } + + private long LookupSyntheticStreamId(long streamTypeValue) + { + return _typedStreams.TryGetValue(streamTypeValue, out var state) + ? state.StreamId + : streamTypeValue; } private void FlushPendingQuicItems( Queue pending, - ConnectionHandle handle) + ConnectionHandle handle, + long streamId) { while (pending.TryDequeue(out var item)) { + if (item is RoutedNetworkBuffer h3) + { + h3.StreamId = streamId; + } + _ = handle.OutboundWriter.WriteAsync(item) .PipeTo(_self, - success: () => new OutboundWriteDone(), - failure: ex => new OutboundWriteFailed(ex.GetBaseException())); + success: static () => new OutboundWriteDone(), + failure: static ex => new OutboundWriteFailed(ex.GetBaseException())); } _ops.OnSignalPullInput(); diff --git a/src/TurboHTTP/Transport/Quic/StreamDirection.cs b/src/Servus.Akka/IO/Quic/StreamDirection.cs similarity index 86% rename from src/TurboHTTP/Transport/Quic/StreamDirection.cs rename to src/Servus.Akka/IO/Quic/StreamDirection.cs index 0f6a9a6e1..c354f3f94 100644 --- a/src/TurboHTTP/Transport/Quic/StreamDirection.cs +++ b/src/Servus.Akka/IO/Quic/StreamDirection.cs @@ -1,13 +1,11 @@ -using TurboHTTP.Transport.Connection; - -namespace TurboHTTP.Transport.Quic; +namespace Servus.Akka.IO.Quic; /// /// Specifies the directionality of a transport stream. /// Used by to allocate only the channels and pipes /// needed for the given direction, avoiding deadlocks on unidirectional QUIC streams. /// -internal enum StreamDirection +public enum StreamDirection { /// Both read and write — standard bidirectional stream (HTTP/1.x, HTTP/2, HTTP/3 request streams). Bidirectional, diff --git a/src/Servus.Akka/IO/Quic/TypedStreamDescriptor.cs b/src/Servus.Akka/IO/Quic/TypedStreamDescriptor.cs new file mode 100644 index 000000000..d80811b96 --- /dev/null +++ b/src/Servus.Akka/IO/Quic/TypedStreamDescriptor.cs @@ -0,0 +1,10 @@ +namespace Servus.Akka.IO.Quic; + +public sealed class TypedStreamState +{ + public ConnectionHandle? Handle; + public readonly Queue PendingItems = new(); + public long StreamId; + public long OriginalSyntheticStreamId; + public bool IsOutbound; +} diff --git a/src/TurboHTTP/Internal/RequestEndpoint.cs b/src/Servus.Akka/IO/RequestEndpoint.cs similarity index 96% rename from src/TurboHTTP/Internal/RequestEndpoint.cs rename to src/Servus.Akka/IO/RequestEndpoint.cs index 055b4c1d9..d3abaff4d 100644 --- a/src/TurboHTTP/Internal/RequestEndpoint.cs +++ b/src/Servus.Akka/IO/RequestEndpoint.cs @@ -1,12 +1,12 @@ using System.Net; -namespace TurboHTTP.Internal; +namespace Servus.Akka.IO; /// /// Identifies a connection target by scheme, host, port, and HTTP version. /// Used as the grouping key for per-host connection pools. /// -internal readonly record struct RequestEndpoint +public readonly record struct RequestEndpoint { /// /// Creates a from the URI and version of . diff --git a/src/TurboHTTP/Transport/Connection/TcpClientProvider.cs b/src/Servus.Akka/IO/Tcp/TcpClientProvider.cs similarity index 79% rename from src/TurboHTTP/Transport/Connection/TcpClientProvider.cs rename to src/Servus.Akka/IO/Tcp/TcpClientProvider.cs index 59c9e0351..5276ce272 100644 --- a/src/TurboHTTP/Transport/Connection/TcpClientProvider.cs +++ b/src/Servus.Akka/IO/Tcp/TcpClientProvider.cs @@ -1,14 +1,14 @@ using System.Diagnostics; using System.Net; using System.Net.Sockets; -using TurboHTTP.Diagnostics; +using Servus.Akka.Diagnostics; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Tcp; /// /// Plain TCP implementation of . /// -internal class TcpClientProvider(TcpOptions options) : IClientProvider +public class TcpClientProvider(TcpOptions options) : IClientProvider { private Socket? _socket; @@ -24,14 +24,13 @@ public async Task GetStreamAsync(CancellationToken ct = default) _socket = CreateSocket(options.SocketSendBufferSize, options.SocketReceiveBufferSize); - var dnsActivity = TurboHttpInstrumentation.StartDnsLookup(connectHost); - TurboHttpEventSource.Instance.DnsLookupStart(connectHost); + var dnsActivity = ServusInstrumentation.StartDnsLookup(connectHost); IPAddress[] addresses; try { var dnsStart = Stopwatch.GetTimestamp(); addresses = await Dns.GetHostAddressesAsync(connectHost, ct).ConfigureAwait(false); - var dnsDurationMs = Stopwatch.GetElapsedTime(dnsStart).TotalMilliseconds; + var dnsDuration = Stopwatch.GetElapsedTime(dnsStart).TotalSeconds; if (addresses.Length == 0) { @@ -40,45 +39,47 @@ public async Task GetStreamAsync(CancellationToken ct = default) if (dnsActivity is not null) { - TurboHttpInstrumentation.SetDnsAnswers(dnsActivity, + ServusInstrumentation.SetDnsAnswers(dnsActivity, Array.ConvertAll(addresses, a => a.ToString())); } - TurboHttpEventSource.Instance.DnsLookupStop(connectHost, dnsDurationMs); - TurboHttpMetrics.DnsLookupDuration.Record(dnsDurationMs / 1000.0, + ServusMetrics.DnsLookupDuration.Record(dnsDuration, new KeyValuePair("dns.question.name", connectHost)); dnsActivity?.Stop(); + ServusTrace.Dns.Debug(this, "DNS '{0}' resolved {1} address(es)", connectHost, addresses.Length); } catch (Exception ex) { if (dnsActivity is not null) { - TurboHttpInstrumentation.SetError(dnsActivity, ex); + ServusInstrumentation.SetError(dnsActivity, ex); dnsActivity.Stop(); } - TurboHttpEventSource.Instance.DnsLookupStop(connectHost, 0); + ServusTrace.Dns.Warning(this, "DNS '{0}' failed: {1}", connectHost, ex.Message); throw; } var networkType = addresses[0].AddressFamily == AddressFamily.InterNetworkV6 ? "ipv6" : "ipv4"; - var socketActivity = TurboHttpInstrumentation.StartSocketConnect( + var socketActivity = ServusInstrumentation.StartSocketConnect( addresses[0].ToString(), connectPort, "tcp", networkType); try { await _socket.ConnectAsync(addresses, connectPort, ct).ConfigureAwait(false); socketActivity?.Stop(); + ServusTrace.Connection.Debug(this, "TCP connected to {0}:{1}", addresses[0], connectPort); } catch (Exception ex) { if (socketActivity is not null) { - TurboHttpInstrumentation.SetError(socketActivity, ex); + ServusInstrumentation.SetError(socketActivity, ex); socketActivity.Stop(); } + ServusTrace.Connection.Warning(this, "TCP connect to {0}:{1} failed: {2}", addresses[0], connectPort, ex.Message); throw; } diff --git a/src/TurboHTTP/Transport/Tcp/DirectConnectionFactory.cs b/src/Servus.Akka/IO/Tcp/TcpConnectionFactory.cs similarity index 68% rename from src/TurboHTTP/Transport/Tcp/DirectConnectionFactory.cs rename to src/Servus.Akka/IO/Tcp/TcpConnectionFactory.cs index 299629b66..c1f8791f2 100644 --- a/src/TurboHTTP/Transport/Tcp/DirectConnectionFactory.cs +++ b/src/Servus.Akka/IO/Tcp/TcpConnectionFactory.cs @@ -1,21 +1,20 @@ using System.Net; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; +using Servus.Akka.Diagnostics; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.Transport.Tcp; +namespace Servus.Akka.IO.Tcp; /// /// Static factory that establishes a TCP/TLS connection, creates channels, /// spawns ByteMover tasks, and returns a — /// all in a single async call with no actor involvement. /// -internal sealed class DirectConnectionFactory : IConnectionFactory +public sealed class TcpConnectionFactory : IConnectionFactory { - public static readonly DirectConnectionFactory Instance = new(); + public static readonly TcpConnectionFactory Instance = new(); - Task IConnectionFactory.EstablishAsync(TcpOptions options, RequestEndpoint endpoint, CancellationToken ct) + Task IConnectionFactory.EstablishAsync(ITransportOptions options, RequestEndpoint endpoint, + CancellationToken ct) => EstablishAsync(options, endpoint, ct); /// @@ -27,7 +26,7 @@ Task IConnectionFactory.EstablishAsync(TcpOptions options, Requ /// Cancellation token for the connection establishment. /// A wrapping the live connection. public static async Task EstablishAsync( - TcpOptions options, + ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct = default) { @@ -37,26 +36,27 @@ public static async Task EstablishAsync( IClientProvider provider = options switch { TlsOptions tls => new TlsClientProvider(tls), - _ => new TcpClientProvider(options) + TcpOptions tcp => new TcpClientProvider(tcp), + _ => throw new ArgumentException($"Unsupported options type: {options.GetType()}", nameof(options)) }; // Start a Connect span that wraps the entire establishment (DNS + socket + TLS) var uri = new Uri($"{(options is TlsOptions ? "https" : "http")}://{endpoint.Host}:{endpoint.Port}/"); - var connectActivity = TurboHttpInstrumentation.StartConnect(uri); - TurboHttpEventSource.Instance.ConnectionStart(endpoint.Host, endpoint.Port); + var connectActivity = ServusInstrumentation.StartConnect(uri); + ServusTrace.Connection.Debug(Instance, "Connecting to {0}:{1}", endpoint.Host, endpoint.Port); try { // 2. Establish TCP/TLS connection var stream = await provider.GetStreamAsync(ct).ConfigureAwait(false); - // Set resolved peer address on the Connect span if (connectActivity is not null && provider.RemoteEndPoint is IPEndPoint remoteEp) { - TurboHttpInstrumentation.SetNetworkPeerAddress(connectActivity, remoteEp.Address.ToString()); + ServusInstrumentation.SetNetworkPeerAddress(connectActivity, remoteEp.Address.ToString()); } connectActivity?.Stop(); + ServusTrace.Connection.Debug(Instance, "Connected to {0}:{1}", endpoint.Host, endpoint.Port); // 3. Create ClientState with channels + Pipe var state = new ClientState( @@ -88,22 +88,21 @@ public static async Task EstablishAsync( _ = ClientByteMover.MoveStreamToChannel(state, onClose, lease.Token); _ = ClientByteMover.MoveChannelToStream(state, onClose, lease.Token); - // 7. Emit connection opened metrics + diagnostics - var protocol = VersionToProtocol(endpoint.Version); - TurboHttpMetrics.OpenConnections.Add(1, + // 7. Emit connection opened metrics + ServusMetrics.OpenConnections.Add(1, new("http.connection.state", "active"), new("server.address", endpoint.Host), new("server.port", endpoint.Port)); - TurboTrace.Connection.Info(typeof(DirectConnectionFactory), "Connection opened: {0}:{1} ({2})", - endpoint.Host, endpoint.Port, protocol); return lease; } catch (Exception ex) { + ServusTrace.Connection.Warning(Instance, "Connection to {0}:{1} failed: {2}", endpoint.Host, endpoint.Port, ex.Message); + if (connectActivity is not null) { - TurboHttpInstrumentation.SetError(connectActivity, ex); + ServusInstrumentation.SetError(connectActivity, ex); connectActivity.Stop(); } @@ -112,12 +111,4 @@ public static async Task EstablishAsync( } } - private static string VersionToProtocol(Version version) => version switch - { - { Major: 1, Minor: 0 } => "HTTP/1.0", - { Major: 1, Minor: 1 } => "HTTP/1.1", - { Major: 2 } => "HTTP/2", - { Major: 3 } => "HTTP/3", - _ => $"HTTP/{version}" - }; } \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/TcpConnectionManagerActor.cs b/src/Servus.Akka/IO/Tcp/TcpConnectionManagerActor.cs similarity index 89% rename from src/TurboHTTP/Transport/Connection/TcpConnectionManagerActor.cs rename to src/Servus.Akka/IO/Tcp/TcpConnectionManagerActor.cs index 9b1ac0fc6..b3ac52719 100644 --- a/src/TurboHTTP/Transport/Connection/TcpConnectionManagerActor.cs +++ b/src/Servus.Akka/IO/Tcp/TcpConnectionManagerActor.cs @@ -1,9 +1,8 @@ using Akka.Actor; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.Diagnostics; +using Servus.Akka.IO.Quic; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Tcp; /// /// Single actor that manages ALL per-host TCP/TLS connection state: acquire, release, idle reuse, @@ -17,15 +16,15 @@ namespace TurboHTTP.Transport.Connection; /// Mirrors structurally — a senior dev who knows /// one immediately understands the other. /// -internal sealed class TcpConnectionManagerActor : ReceiveActor, IWithTimers +public sealed class TcpConnectionManagerActor : ReceiveActor, IWithTimers { - internal sealed record Acquire( - TcpOptions Options, + public sealed record Acquire( + ITransportOptions Options, RequestEndpoint Endpoint, TaskCompletionSource Tcs, CancellationToken Token); - internal sealed record Release(ConnectionLease Lease, bool CanReuse); + public sealed record Release(ConnectionLease Lease, bool CanReuse); private sealed record Established(ConnectionLease Lease, Acquire Original); @@ -74,7 +73,7 @@ public HostState(RequestEndpoint endpoint, int maxConnectionsPerServer) /// the actor skips already-completed TCS instances on dequeue. /// public static Task AcquireAsync( - IActorRef actor, TcpOptions options, RequestEndpoint endpoint, CancellationToken ct = default) + IActorRef actor, ITransportOptions options, RequestEndpoint endpoint, CancellationToken ct = default) { var tcs = new TaskCompletionSource(); @@ -90,7 +89,7 @@ public static Task AcquireAsync( } public TcpConnectionManagerActor(TimeSpan idleTimeout, TimeSpan connectionLifetime, int maxConnectionsPerServer = 6) - : this(DirectConnectionFactory.Instance, idleTimeout, connectionLifetime, maxConnectionsPerServer) + : this(TcpConnectionFactory.Instance, idleTimeout, connectionLifetime, maxConnectionsPerServer) { } @@ -166,7 +165,8 @@ private void OnAcquire(Acquire msg) } else { - TurboHttpMetrics.OpenConnections.Add(-1, + ServusTrace.Pool.Debug(this, "Idle connection reused for {0}:{1}", host.Endpoint.Host, host.Endpoint.Port); + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "idle"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -178,7 +178,7 @@ private void OnAcquire(Acquire msg) // Stale — dispose and free the slot host.Leases.Remove(idle); idle.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -211,7 +211,7 @@ private void OnRelease(Release msg) { host.Leases.Remove(msg.Lease); msg.Lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -223,7 +223,7 @@ private void OnRelease(Release msg) { host.Leases.Remove(msg.Lease); msg.Lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -249,7 +249,7 @@ private void OnRelease(Release msg) // No pending callers — park in idle pool host.Idle.Enqueue(msg.Lease); - TurboHttpMetrics.OpenConnections.Add(1, + ServusMetrics.OpenConnections.Add(1, new("http.connection.state", "idle"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -259,7 +259,7 @@ private void OnRelease(Release msg) // Not reusable — dispose and free the slot host.Leases.Remove(msg.Lease); msg.Lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -274,7 +274,7 @@ private void OnEstablished(Established msg) host.Establishing--; host.Leases.Add(msg.Lease); msg.Lease.MarkBusy(); - TurboHttpMetrics.OpenConnections.Add(1, + ServusMetrics.OpenConnections.Add(1, new("http.connection.state", "active"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -363,11 +363,17 @@ private void EvictHost(HostState host) host.Idle.Enqueue(item); } + if (expired.Count > 0) + { + ServusTrace.Pool.Debug(this, "Evicting {0} idle connection(s) from pool for {1}:{2}", + expired.Count, host.Endpoint.Host, host.Endpoint.Port); + } + foreach (var lease in expired) { host.Leases.Remove(lease); lease.Dispose(); - TurboHttpMetrics.OpenConnections.Add(-1, + ServusMetrics.OpenConnections.Add(-1, new("http.connection.state", "idle"), new("server.address", host.Endpoint.Host), new("server.port", host.Endpoint.Port)); @@ -414,6 +420,8 @@ private HostState GetOrCreateHost(RequestEndpoint endpoint) private void Establish(HostState host, Acquire msg) { host.Establishing++; + ServusTrace.Pool.Debug(this, "Establishing connection to {0}:{1} (establishing={2})", + host.Endpoint.Host, host.Endpoint.Port, host.Establishing); _ = _factory .EstablishAsync(msg.Options, msg.Endpoint, msg.Token) .PipeTo(Self, diff --git a/src/TurboHTTP/Transport/Tcp/TcpConnectionStage.cs b/src/Servus.Akka/IO/Tcp/TcpConnectionStage.cs similarity index 88% rename from src/TurboHTTP/Transport/Tcp/TcpConnectionStage.cs rename to src/Servus.Akka/IO/Tcp/TcpConnectionStage.cs index 62abcdd34..ac8f91102 100644 --- a/src/TurboHTTP/Transport/Tcp/TcpConnectionStage.cs +++ b/src/Servus.Akka/IO/Tcp/TcpConnectionStage.cs @@ -2,11 +2,10 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; -namespace TurboHTTP.Transport.Tcp; +namespace Servus.Akka.IO.Tcp; -internal interface ITransportOperations +public interface ITransportOperations { void OnPushOutput(IInputItem item); void OnSignalPullInput(); @@ -16,20 +15,18 @@ internal interface ITransportOperations ILoggingAdapter Log { get; } } -internal sealed class TcpConnectionStage : GraphStage> +public sealed class TcpConnectionStage : GraphStage> { private IActorRef ConnectionManager { get; } - private TurboClientOptions ClientOptions { get; } private readonly Inlet _in = new("TcpConnection.In"); private readonly Outlet _out = new("TcpConnection.Out"); public override FlowShape Shape { get; } - public TcpConnectionStage(IActorRef connectionManager, TurboClientOptions clientOptions) + public TcpConnectionStage(IActorRef connectionManager) { ConnectionManager = connectionManager; - ClientOptions = clientOptions; Shape = new FlowShape(_in, _out); } @@ -71,7 +68,6 @@ public override void PreStart() _sm = new TcpTransportStateMachine( this, _stage.ConnectionManager, - _stage.ClientOptions, stageActor.Ref); Pull(_stage._in); } @@ -118,4 +114,4 @@ void ITransportOperations.OnScheduleTimer(string key, TimeSpan delay) ILoggingAdapter ITransportOperations.Log => Log; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/TcpOptions.cs b/src/Servus.Akka/IO/Tcp/TcpOptions.cs similarity index 87% rename from src/TurboHTTP/Transport/Connection/TcpOptions.cs rename to src/Servus.Akka/IO/Tcp/TcpOptions.cs index 8bf65625f..facfeaa47 100644 --- a/src/TurboHTTP/Transport/Connection/TcpOptions.cs +++ b/src/Servus.Akka/IO/Tcp/TcpOptions.cs @@ -1,11 +1,11 @@ using System.Net; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Tcp; /// /// Configuration options for a plain TCP connection. /// -internal record TcpOptions +public record TcpOptions : ITransportOptions { public required string Host { get; init; } public required int Port { get; init; } diff --git a/src/Servus.Akka/IO/Tcp/TcpPumpManager.cs b/src/Servus.Akka/IO/Tcp/TcpPumpManager.cs new file mode 100644 index 000000000..3dc0ecaed --- /dev/null +++ b/src/Servus.Akka/IO/Tcp/TcpPumpManager.cs @@ -0,0 +1,114 @@ +using System.Buffers; +using System.Threading.Channels; +using Akka.Actor; + +namespace Servus.Akka.IO.Tcp; + +/// +/// Manages the lifecycle of the TCP inbound pump — start, cancel, and the async read loop +/// that marshals batches of into StageActorRef messages. +/// Extracted from for single-responsibility. +/// +internal sealed class TcpPumpManager +{ + private readonly IActorRef _self; + private CancellationTokenSource? _pumpCts; + + public TcpPumpManager(IActorRef self) + { + _self = self; + } + + public void StartInboundPump(ConnectionHandle handle, RequestEndpoint key, int gen) + { + StopInboundPump(); + + _pumpCts = new CancellationTokenSource(); + _ = PumpAsync(handle.InboundReader, key, gen, _pumpCts.Token, _self); + } + + public void StopInboundPump() + { + if (_pumpCts is null) + { + return; + } + + _pumpCts.Cancel(); + _pumpCts.Dispose(); + _pumpCts = null; + } + + private static async Task PumpAsync( + ChannelReader reader, + RequestEndpoint key, + int gen, + CancellationToken ct, + IActorRef self) + { + var closeKind = TlsCloseKind.CleanClose; + try + { + while (await reader.WaitToReadAsync(ct).ConfigureAwait(false)) + { + IInputItem[]? batch = null; + var count = 0; + + while (reader.TryRead(out var chunk)) + { + // Early exit when the connection generation changed — the actor thread + // always cancels the pump CTS after incrementing _connectionGen, so + // checking the token is sufficient. This avoids a cross-thread volatile + // read of _connectionGen from the pump's ThreadPool thread. + if (ct.IsCancellationRequested) + { + chunk.Dispose(); + while (reader.TryRead(out var stale)) { stale.Dispose(); } + if (batch is not null) { ArrayPool.Shared.Return(batch); } + return; + } + + chunk.Key = key; + batch ??= ArrayPool.Shared.Rent(8); + + if (count == batch.Length) + { + self.Tell(new InboundBatch(batch, count, gen)); + batch = ArrayPool.Shared.Rent(count * 2); + count = 0; + } + + batch[count++] = chunk; + } + + if (count > 0) + { + self.Tell(new InboundBatch(batch!, count, gen)); + } + else if (batch is not null) + { + ArrayPool.Shared.Return(batch); + } + } + } + catch (OperationCanceledException) + { + return; + } + catch (AbruptCloseException) + { + closeKind = TlsCloseKind.AbruptClose; + } + catch (ChannelClosedException ex) when (ex.InnerException is AbruptCloseException) + { + closeKind = TlsCloseKind.AbruptClose; + } + catch (Exception ex) + { + self.Tell(new InboundPumpFailed(ex)); + return; + } + + self.Tell(new InboundComplete(closeKind, gen)); + } +} diff --git a/src/Servus.Akka/IO/Tcp/TcpTransportEvent.cs b/src/Servus.Akka/IO/Tcp/TcpTransportEvent.cs new file mode 100644 index 000000000..d73a39d24 --- /dev/null +++ b/src/Servus.Akka/IO/Tcp/TcpTransportEvent.cs @@ -0,0 +1,19 @@ +namespace Servus.Akka.IO.Tcp; + +public readonly record struct LeaseAcquired(ConnectionLease Lease) : ITcpTransportEvent; + +public readonly record struct AcquisitionFailed(Exception Error) : ITcpTransportEvent; + +public readonly record struct InboundBatch(IInputItem[] Batch, int Count, int Gen) : ITcpTransportEvent; + +public readonly record struct InboundComplete(TlsCloseKind CloseKind, int Gen) : ITcpTransportEvent; + +public readonly record struct InboundPumpFailed(Exception Error) : ITcpTransportEvent; + +public readonly record struct OutboundWriteDone : ITcpTransportEvent; + +public readonly record struct OutboundWriteFailed(Exception Error) : ITcpTransportEvent; + +public readonly record struct FlushNextCompleted : ITcpTransportEvent; + +public interface ITcpTransportEvent; \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Tcp/TcpTransportFactory.cs b/src/Servus.Akka/IO/Tcp/TcpTransportFactory.cs similarity index 66% rename from src/TurboHTTP/Transport/Tcp/TcpTransportFactory.cs rename to src/Servus.Akka/IO/Tcp/TcpTransportFactory.cs index 3a99c8df6..1be41a38f 100644 --- a/src/TurboHTTP/Transport/Tcp/TcpTransportFactory.cs +++ b/src/Servus.Akka/IO/Tcp/TcpTransportFactory.cs @@ -1,30 +1,25 @@ using Akka; using Akka.Actor; using Akka.Streams.Dsl; -using TurboHTTP.Internal; -using TurboHTTP.Streams; -namespace TurboHTTP.Transport.Tcp; +namespace Servus.Akka.IO.Tcp; /// /// Transport factory for TCP/TLS connections (HTTP/1.0, HTTP/1.1, HTTP/2). /// Encapsulates connection management and client options, creating a new /// on demand. /// -internal sealed class TcpTransportFactory : ITransportFactory +public sealed class TcpTransportFactory : ITransportFactory { private readonly IActorRef _connectionManager; - private readonly TurboClientOptions _clientOptions; /// /// Initializes a new instance of the class. /// /// Actor reference for managing TCP connection lifecycle - /// Client configuration options - public TcpTransportFactory(IActorRef connectionManager, TurboClientOptions clientOptions) + public TcpTransportFactory(IActorRef connectionManager) { _connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager)); - _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); } /// @@ -33,6 +28,6 @@ public TcpTransportFactory(IActorRef connectionManager, TurboClientOptions clien /// A flow wrapping a . public Flow Create() { - return Flow.FromGraph(new TcpConnectionStage(_connectionManager, _clientOptions)); + return Flow.FromGraph(new TcpConnectionStage(_connectionManager)); } } diff --git a/src/TurboHTTP/Transport/Tcp/TcpTransportStateMachine.cs b/src/Servus.Akka/IO/Tcp/TcpTransportStateMachine.cs similarity index 69% rename from src/TurboHTTP/Transport/Tcp/TcpTransportStateMachine.cs rename to src/Servus.Akka/IO/Tcp/TcpTransportStateMachine.cs index 3bed01544..fe32fe26c 100644 --- a/src/TurboHTTP/Transport/Tcp/TcpTransportStateMachine.cs +++ b/src/Servus.Akka/IO/Tcp/TcpTransportStateMachine.cs @@ -1,13 +1,10 @@ using System.Buffers; using System.Diagnostics; -using System.Threading.Channels; using Akka.Actor; using Akka.Event; -using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; +using Servus.Akka.Diagnostics; -namespace TurboHTTP.Transport.Tcp; +namespace Servus.Akka.IO.Tcp; /// /// Encapsulates all TCP/TLS transport state and logic — connection acquisition, inbound pumping, @@ -16,13 +13,12 @@ namespace TurboHTTP.Transport.Tcp; /// (Push, Pull, Timer, Complete, Fail). /// Async events arrive via after being marshaled through the StageActorRef. /// -internal sealed class TcpTransportStateMachine +public sealed class TcpTransportStateMachine { private const string ConnectTimerKey = "connect-timeout"; private readonly ITransportOperations _ops; private readonly IActorRef _connectionManager; - private readonly TurboClientOptions _clientOptions; private readonly IActorRef _self; private ConnectionHandle? _handle; @@ -46,19 +42,18 @@ internal sealed class TcpTransportStateMachine private bool _upstreamFinished; private bool _isReconnecting; - private CancellationTokenSource? _pumpCts; + private readonly TcpPumpManager _pumpManager; private CancellationTokenSource? _acquireCts; public TcpTransportStateMachine( ITransportOperations ops, IActorRef connectionManager, - TurboClientOptions clientOptions, IActorRef self) { _ops = ops; _connectionManager = connectionManager; - _clientOptions = clientOptions; _self = self; + _pumpManager = new TcpPumpManager(self); } public void Dispatch(ITcpTransportEvent evt) @@ -103,17 +98,6 @@ public void Dispatch(ITcpTransportEvent evt) public void HandlePush(IOutputItem item) { - // Auto-connect: on the first data/control item, derive connection parameters - // from the item's endpoint and acquire a connection. Skip ConnectItem — it - // handles its own acquisition in HandleConnectItem and running AutoConnect - // first would start a duplicate acquire that races with the real one. - if (_handle is null && _pendingConnect is null && item is not ConnectItem && - !string.IsNullOrEmpty(item.Key.Scheme) && - item.Key != RequestEndpoint.Default) - { - AutoConnect(item.Key); - } - switch (item) { case ConnectItem connect: @@ -138,10 +122,6 @@ public void HandlePush(IOutputItem item) _pendingResponseCount++; _ops.OnSignalPullInput(); break; - - case ReconnectItem reconnectItem: - HandleReconnectItem(reconnectItem); - break; } } @@ -158,7 +138,7 @@ public void HandleUpstreamFinish() // Complete now; otherwise we'd wait forever since no more // ConnectionReuseItem signals will arrive. _connectionGen++; - StopInboundPump(); + _pumpManager.StopInboundPump(); ReturnLeaseToPool(canReuse: true); _handle = null; _currentLease = null; @@ -172,35 +152,23 @@ public void HandleDownstreamFinish() CleanupTransport(); } - private void HandleReconnectItem(ReconnectItem reconnectItem) - { - _ops.Log.Debug("TcpConnectionStage: ReconnectItem — tearing down and reconnecting to {0}:{1}", - reconnectItem.Key.Host, reconnectItem.Key.Port); - - _isReconnecting = true; - CleanupTransport(); - - var options = OptionsFactory.Build(reconnectItem.Key, _clientOptions); - _pendingConnect = new ConnectItem(options) { Key = reconnectItem.Key }; - AcquireConnection(_pendingConnect.Value); - } - - private void AutoConnect(RequestEndpoint endpoint) - { - _ops.Log.Debug("TcpConnectionStage: AutoConnect for {0}:{1}", endpoint.Host, endpoint.Port); - - var options = OptionsFactory.Build(endpoint, _clientOptions); - _pendingConnect = new ConnectItem(options) { Key = endpoint }; - AcquireConnection(_pendingConnect.Value); - } - private void HandleConnectItem(ConnectItem connect) { - _ops.Log.Debug("TcpConnectionStage: ConnectItem key={0}:{1}", connect.Key.Host, connect.Key.Port); + if (connect.IsReconnect) + { + _ops.Log.Debug("TcpConnectionStage: ConnectItem (reconnect) key={0}:{1}", connect.Key.Host, + connect.Key.Port); + _isReconnecting = true; + } + else + { + _ops.Log.Debug("TcpConnectionStage: ConnectItem key={0}:{1}", connect.Key.Host, connect.Key.Port); + } CleanupTransport(); - _pendingConnect = connect; - AcquireConnection(connect); + _pendingConnect = connect with { Options = connect.Options! }; + AcquireConnection(_pendingConnect.Value); + _ops.OnSignalPullInput(); } private void HandleBuffer(NetworkBuffer buffer) @@ -221,16 +189,16 @@ private void HandleBuffer(NetworkBuffer buffer) private void HandleConnectionReuseItem(ConnectionReuseItem reuseItem) { _ops.Log.Debug("TcpConnectionStage: ConnectionReuseItem canReuse={0}, pendingResponseCount={1}", - reuseItem.Decision.CanReuse, _pendingResponseCount); + reuseItem.CanReuse, _pendingResponseCount); - if (!reuseItem.Decision.CanReuse) + if (!reuseItem.CanReuse) { _pendingResponseCount = 0; _currentLease?.MarkNoReuse(); _leaseReturned = false; ReturnLeaseToPool(canReuse: false); _connectionGen++; - StopInboundPump(); + _pumpManager.StopInboundPump(); _handle = null; _currentLease = null; } @@ -255,7 +223,7 @@ private void HandleConnectionReuseItem(ConnectionReuseItem reuseItem) if (_handle is not null) { _connectionGen++; - StopInboundPump(); + _pumpManager.StopInboundPump(); _handle = null; _currentLease = null; } @@ -272,25 +240,25 @@ public void OnTimer(string? timerKey) switch (timerKey) { case ConnectTimerKey: - { - if (_pendingConnect is null) { - return; - } + if (_pendingConnect is null) + { + return; + } - _ops.Log.Warning("TcpConnectionStage: Connection acquisition timed out for {0}:{1}", - _pendingConnect.Value.Key.Host, _pendingConnect.Value.Key.Port); + _ops.Log.Warning("TcpConnectionStage: Connection acquisition timed out for {0}:{1}", + _pendingConnect.Value.Key.Host, _pendingConnect.Value.Key.Port); - _waitActivity?.Stop(); - _waitActivity = null; + _waitActivity?.Stop(); + _waitActivity = null; - var signal = new CloseSignalItem(TlsCloseKind.AbruptClose) { Key = _pendingConnect.Value.Key }; - _pendingConnect = null; + var signal = new CloseSignalItem(TlsCloseKind.AbruptClose) { Key = _pendingConnect.Value.Key }; + _pendingConnect = null; - _ops.OnPushOutput(signal); - _ops.OnSignalPullInput(); - break; - } + _ops.OnPushOutput(signal); + _ops.OnSignalPullInput(); + break; + } } } @@ -301,7 +269,7 @@ private void OnLeaseAcquired(ConnectionLease lease) _waitActivity?.Stop(); _waitActivity = null; var waitDurationS = Stopwatch.GetElapsedTime(_acquireTimestamp).TotalSeconds; - TurboHttpMetrics.RequestTimeInQueue.Record(waitDurationS, + ServusMetrics.RequestTimeInQueue.Record(waitDurationS, new("server.address", lease.Key.Host), new("server.port", lease.Key.Port)); @@ -323,7 +291,7 @@ private void OnLeaseAcquired(ConnectionLease lease) _handle = lease.Handle; _currentKey = lease.Key; - StartInboundPump(); + _pumpManager.StartInboundPump(_handle, _currentKey, _connectionGen); if (_isReconnecting) { @@ -349,7 +317,7 @@ private void OnOutboundWriteFailed(Exception ex) var signal = new CloseSignalItem(TlsCloseKind.AbruptClose) { Key = _currentKey }; _ops.OnPushOutput(signal); - StopInboundPump(); + _pumpManager.StopInboundPump(); _handle = null; _currentLease = null; @@ -371,7 +339,7 @@ private void OnAcquisitionFailed(Exception ex) if (_waitActivity is not null) { - TurboHttpInstrumentation.SetError(_waitActivity, ex); + ServusInstrumentation.SetError(_waitActivity, ex); _waitActivity.Stop(); _waitActivity = null; } @@ -442,7 +410,7 @@ private void AcquireConnection(ConnectItem connect) _acquireCts?.Dispose(); _acquireCts = new CancellationTokenSource(); - _waitActivity = TurboHttpInstrumentation.StartWaitForConnection( + _waitActivity = ServusInstrumentation.StartWaitForConnection( connect.Key.Host, connect.Key.Port); _acquireTimestamp = Stopwatch.GetTimestamp(); @@ -454,7 +422,7 @@ private void AcquireConnection(ConnectItem connect) failure: ex => new AcquisitionFailed(ex.GetBaseException())); const int defaultConnectTimeoutSeconds = 10; - var timeout = connect.Options.ConnectTimeout; + var timeout = connect.Options!.ConnectTimeout; if (timeout <= TimeSpan.Zero) { timeout = TimeSpan.FromSeconds(defaultConnectTimeoutSeconds); @@ -478,7 +446,7 @@ private void CleanupTransport() { _ops.Log.Debug("TcpConnectionStage: CleanupTransport gen={0}", _connectionGen); _connectionGen++; - StopInboundPump(); + _pumpManager.StopInboundPump(); // Cancel any in-flight connection acquisition so the ConnectionManager // can immediately release the lease instead of sending it to dead letters. @@ -501,107 +469,6 @@ private void CleanupTransport() } } - private void StartInboundPump() - { - StopInboundPump(); - - var handle = _handle; - if (handle is null) - { - return; - } - - _pumpCts = new CancellationTokenSource(); - var ct = _pumpCts.Token; - var reader = handle.InboundReader; - var key = _currentKey; - var gen = _connectionGen; - - _ = PumpAsync(reader, key, gen, ct, _self); - } - - private static async Task PumpAsync( - ChannelReader reader, - RequestEndpoint key, - int gen, - CancellationToken ct, - IActorRef self) - { - var closeKind = TlsCloseKind.CleanClose; - try - { - while (await reader.WaitToReadAsync(ct).ConfigureAwait(false)) - { - IInputItem[]? batch = null; - var count = 0; - - while (reader.TryRead(out var chunk)) - { - // Early exit when the connection generation changed — the actor thread - // always cancels the pump CTS after incrementing _connectionGen, so - // checking the token is sufficient. This avoids a cross-thread volatile - // read of _connectionGen from the pump's ThreadPool thread. - if (ct.IsCancellationRequested) - { - chunk.Dispose(); - while (reader.TryRead(out var stale)) stale.Dispose(); - if (batch is not null) ArrayPool.Shared.Return(batch); - return; - } - - chunk.Key = key; - batch ??= ArrayPool.Shared.Rent(8); - - if (count == batch.Length) - { - self.Tell(new InboundBatch(batch, count, gen)); - batch = ArrayPool.Shared.Rent(count * 2); - count = 0; - } - - batch[count++] = chunk; - } - - if (count > 0) - { - self.Tell(new InboundBatch(batch!, count, gen)); - } - else if (batch is not null) - { - ArrayPool.Shared.Return(batch); - } - } - } - catch (OperationCanceledException) - { - return; - } - catch (ChannelClosedException ex) when (ex.InnerException is AbruptCloseException) - { - closeKind = TlsCloseKind.AbruptClose; - } - catch (Exception ex) - { - self.Tell(new InboundPumpFailed(ex)); - return; - } - - self.Tell(new InboundComplete(closeKind, gen)); - } - - private void StopInboundPump() - { - if (_pumpCts is null) - { - return; - } - - _ops.Log.Debug("TcpConnectionStage: StopInboundPump gen={0}", _connectionGen); - _pumpCts.Cancel(); - _pumpCts.Dispose(); - _pumpCts = null; - } - private void WriteToOutbound(NetworkBuffer buffer) { _handle!.OutboundWriter.WriteAsync(buffer) diff --git a/src/TurboHTTP/Transport/Connection/TlsClientProvider.cs b/src/Servus.Akka/IO/Tcp/TlsClientProvider.cs similarity index 86% rename from src/TurboHTTP/Transport/Connection/TlsClientProvider.cs rename to src/Servus.Akka/IO/Tcp/TlsClientProvider.cs index 6c60c73e4..e9764c71b 100644 --- a/src/TurboHTTP/Transport/Connection/TlsClientProvider.cs +++ b/src/Servus.Akka/IO/Tcp/TlsClientProvider.cs @@ -1,16 +1,15 @@ -using System.Diagnostics; -using System.Net; +using System.Net; using System.Net.Security; using System.Security.Authentication; -using TurboHTTP.Diagnostics; +using Servus.Akka.Diagnostics; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Tcp; /// /// TLS-wrapped implementation of . Establishes a plain TCP connection /// first and then performs TLS handshake using . /// -internal class TlsClientProvider(TlsOptions options) : IClientProvider +public class TlsClientProvider(TlsOptions options) : IClientProvider { private readonly TcpClientProvider _tcpClientProvider = new(options); private SslStream? _sslStream; @@ -47,17 +46,14 @@ await EstablishConnectTunnelAsync(networkStream, options.Host, options.Port, ApplicationProtocols = options.ApplicationProtocols, }; - var tlsActivity = TurboHttpInstrumentation.StartTlsHandshake(targetHost); - TurboHttpEventSource.Instance.TlsHandshakeStart(targetHost); - var tlsStart = Stopwatch.GetTimestamp(); + var tlsActivity = ServusInstrumentation.StartTlsHandshake(targetHost); + ServusTrace.Tls.Debug(this, "TLS handshake starting with '{0}'", targetHost); try { await _sslStream.AuthenticateAsClientAsync(authOptions, ct) .WaitAsync(options.ConnectTimeout, ct) .ConfigureAwait(false); - var tlsDurationMs = Stopwatch.GetElapsedTime(tlsStart).TotalMilliseconds; - if (tlsActivity is not null) { var protocolVersion = _sslStream.SslProtocol switch @@ -66,22 +62,21 @@ await _sslStream.AuthenticateAsClientAsync(authOptions, ct) SslProtocols.Tls13 => "1.3", _ => _sslStream.SslProtocol.ToString() }; - TurboHttpInstrumentation.SetTlsInfo(tlsActivity, "tls", protocolVersion); + ServusInstrumentation.SetTlsInfo(tlsActivity, "tls", protocolVersion); tlsActivity.Stop(); } - TurboHttpEventSource.Instance.TlsHandshakeStop(targetHost, tlsDurationMs); + ServusTrace.Tls.Debug(this, "TLS handshake completed with '{0}'", targetHost); } catch (Exception ex) { if (tlsActivity is not null) { - TurboHttpInstrumentation.SetError(tlsActivity, ex); + ServusInstrumentation.SetError(tlsActivity, ex); tlsActivity.Stop(); } - var tlsDurationMs = Stopwatch.GetElapsedTime(tlsStart).TotalMilliseconds; - TurboHttpEventSource.Instance.TlsHandshakeStop(targetHost, tlsDurationMs); + ServusTrace.Tls.Warning(this, "TLS handshake with '{0}' failed: {1}", targetHost, ex.Message); throw; } @@ -92,7 +87,7 @@ await _sslStream.AuthenticateAsClientAsync(authOptions, ct) /// Sends an HTTP CONNECT request through the proxy to establish a tunnel to the target host. /// RFC 9110 §9.3.6: the CONNECT method requests that the proxy establish a tunnel. /// - internal static async Task EstablishConnectTunnelAsync( + public static async Task EstablishConnectTunnelAsync( Stream proxyStream, string targetHost, int targetPort, diff --git a/src/TurboHTTP/Transport/Connection/TlsOptions.cs b/src/Servus.Akka/IO/Tcp/TlsOptions.cs similarity index 88% rename from src/TurboHTTP/Transport/Connection/TlsOptions.cs rename to src/Servus.Akka/IO/Tcp/TlsOptions.cs index 070ad5275..26aa827fb 100644 --- a/src/TurboHTTP/Transport/Connection/TlsOptions.cs +++ b/src/Servus.Akka/IO/Tcp/TlsOptions.cs @@ -2,12 +2,12 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace TurboHTTP.Transport.Connection; +namespace Servus.Akka.IO.Tcp; /// /// TLS connection options, extending with certificate and protocol settings. /// -internal record TlsOptions : TcpOptions +public record TlsOptions : TcpOptions { public string? TargetHost { get; init; } public X509CertificateCollection? ClientCertificates { get; init; } diff --git a/src/Servus.Akka/Servus.Akka.csproj b/src/Servus.Akka/Servus.Akka.csproj new file mode 100644 index 000000000..3ba67f7b2 --- /dev/null +++ b/src/Servus.Akka/Servus.Akka.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index dd00a4b1f..ae3253e68 100644 --- a/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -208,24 +208,17 @@ namespace TurboHTTP.Diagnostics public enum TurboTraceCategory : ushort { None = 0, - Connection = 1, Protocol = 2, Request = 4, - Response = 8, Cache = 16, Redirect = 32, Retry = 64, - Pool = 128, - Transport = 256, - Stream = 512, - All = 1023, + All = 118, } public static class TurboTraceExtensions { - public static OpenTelemetry.Metrics.MeterProviderBuilder AddTurboHttpMetrics(this OpenTelemetry.Metrics.MeterProviderBuilder builder) { } - public static OpenTelemetry.Trace.TracerProviderBuilder AddTurboHttpTracing(this OpenTelemetry.Trace.TracerProviderBuilder builder) { } - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboLoggerTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, TurboHTTP.Diagnostics.TurboTraceCategory categories = 1023, TurboHTTP.Diagnostics.TurboTraceLevel minimumLevel = 1) { } - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, TurboHTTP.Diagnostics.ITurboTraceListener listener, TurboHTTP.Diagnostics.TurboTraceCategory categories = 1023, TurboHTTP.Diagnostics.TurboTraceLevel minimumLevel = 1) { } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboLoggerTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, TurboHTTP.Diagnostics.TurboTraceCategory categories = 118, TurboHTTP.Diagnostics.TurboTraceLevel minimumLevel = 1) { } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, TurboHTTP.Diagnostics.ITurboTraceListener listener, TurboHTTP.Diagnostics.TurboTraceCategory categories = 118, TurboHTTP.Diagnostics.TurboTraceLevel minimumLevel = 1) { } } public enum TurboTraceLevel : byte { diff --git a/src/TurboHTTP.AcceptanceTests/H10/CompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/CompressionSpec.cs index 1264d720b..9ea511dee 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/CompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/CompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H10/ConcurrencySpec.cs b/src/TurboHTTP.AcceptanceTests/H10/ConcurrencySpec.cs index 85c8091dd..785d6129c 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/ConcurrencySpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/ConcurrencySpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H10; diff --git a/src/TurboHTTP.AcceptanceTests/H10/ConnectionSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/ConnectionSpec.cs index 012551299..4734073a2 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/ConnectionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/ConnectionSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H10; diff --git a/src/TurboHTTP.AcceptanceTests/H10/EdgeCaseSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/EdgeCaseSpec.cs index 1da18de01..9e1605b7f 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/EdgeCaseSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/EdgeCaseSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H10; diff --git a/src/TurboHTTP.AcceptanceTests/H10/ErrorHandlingSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/ErrorHandlingSpec.cs index cb4ac7c66..63da73737 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/ErrorHandlingSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H10; diff --git a/src/TurboHTTP.AcceptanceTests/H10/ExpectContinueSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/ExpectContinueSpec.cs index c4fb327be..94b9c643f 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/ExpectContinueSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/ExpectContinueSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H10/RequestCompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/RequestCompressionSpec.cs index 28dc27ff7..f54c8faf6 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/RequestCompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/RequestCompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H10/ResilienceSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/ResilienceSpec.cs index 36a768740..ea5fc27cd 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/ResilienceSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/ResilienceSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H10/SmokeSpec.cs b/src/TurboHTTP.AcceptanceTests/H10/SmokeSpec.cs index 51808c1c0..f424725d3 100644 --- a/src/TurboHTTP.AcceptanceTests/H10/SmokeSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H10/SmokeSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H10; diff --git a/src/TurboHTTP.AcceptanceTests/H11/CompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/CompressionSpec.cs index 8ded08cc9..59c2963de 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/CompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/CompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -13,7 +13,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class CompressionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateDecompressingEngine() diff --git a/src/TurboHTTP.AcceptanceTests/H11/ConcurrencySpec.cs b/src/TurboHTTP.AcceptanceTests/H11/ConcurrencySpec.cs index 5460754db..2c20c4784 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/ConcurrencySpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/ConcurrencySpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class ConcurrencySpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK) { diff --git a/src/TurboHTTP.AcceptanceTests/H11/ConnectionSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/ConnectionSpec.cs index 4c4aba1ac..f6ef680f0 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/ConnectionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/ConnectionSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class ConnectionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK, string? extraHeaders = null) diff --git a/src/TurboHTTP.AcceptanceTests/H11/EdgeCaseSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/EdgeCaseSpec.cs index d5332b8ae..5e9a4ad88 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/EdgeCaseSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/EdgeCaseSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class EdgeCaseSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(byte[] body, HttpStatusCode status = HttpStatusCode.OK, string? extraHeaders = null) diff --git a/src/TurboHTTP.AcceptanceTests/H11/ErrorHandlingSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/ErrorHandlingSpec.cs index 4db323845..4443b8379 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/ErrorHandlingSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class ErrorHandlingSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK, string? extraHeaders = null) diff --git a/src/TurboHTTP.AcceptanceTests/H11/ExpectContinueSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/ExpectContinueSpec.cs index 5c5a4880d..81f37d697 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/ExpectContinueSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/ExpectContinueSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; @@ -13,7 +13,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class ExpectContinueSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateExpectContinueEngine() diff --git a/src/TurboHTTP.AcceptanceTests/H11/RequestCompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/RequestCompressionSpec.cs index c0248a9dd..7da99a309 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/RequestCompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/RequestCompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; @@ -14,7 +14,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class RequestCompressionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] MakePayload(int size) { diff --git a/src/TurboHTTP.AcceptanceTests/H11/ResilienceSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/ResilienceSpec.cs index 17e740bb1..987a172d5 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/ResilienceSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/ResilienceSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -12,7 +12,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class ResilienceSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateDecompressingEngine() diff --git a/src/TurboHTTP.AcceptanceTests/H11/SmokeSpec.cs b/src/TurboHTTP.AcceptanceTests/H11/SmokeSpec.cs index 12904b5f3..0ad008573 100644 --- a/src/TurboHTTP.AcceptanceTests/H11/SmokeSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H11/SmokeSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.H11; public sealed class SmokeSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); [Fact(Timeout = 5000)] [Trait("RFC", "RFC9110-15.3")] diff --git a/src/TurboHTTP.AcceptanceTests/H2/CompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H2/CompressionSpec.cs index 3b1adb22e..5fe334ec7 100644 --- a/src/TurboHTTP.AcceptanceTests/H2/CompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H2/CompressionSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H2/ErrorHandlingSpec.cs b/src/TurboHTTP.AcceptanceTests/H2/ErrorHandlingSpec.cs index 8844bc57f..eec84b10e 100644 --- a/src/TurboHTTP.AcceptanceTests/H2/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H2/ErrorHandlingSpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H2/ExpectContinueSpec.cs b/src/TurboHTTP.AcceptanceTests/H2/ExpectContinueSpec.cs index b86af22e5..78ba150c5 100644 --- a/src/TurboHTTP.AcceptanceTests/H2/ExpectContinueSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H2/ExpectContinueSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H2/RequestCompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H2/RequestCompressionSpec.cs index 92c7488d0..08a007688 100644 --- a/src/TurboHTTP.AcceptanceTests/H2/RequestCompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H2/RequestCompressionSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H2/ResilienceSpec.cs b/src/TurboHTTP.AcceptanceTests/H2/ResilienceSpec.cs index 860b747d8..f1f49195c 100644 --- a/src/TurboHTTP.AcceptanceTests/H2/ResilienceSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H2/ResilienceSpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H2; diff --git a/src/TurboHTTP.AcceptanceTests/H3/CompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H3/CompressionSpec.cs index 6200e86cd..44725e1c4 100644 --- a/src/TurboHTTP.AcceptanceTests/H3/CompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H3/CompressionSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H3/ErrorHandlingSpec.cs b/src/TurboHTTP.AcceptanceTests/H3/ErrorHandlingSpec.cs index 84b02674f..6f28339cb 100644 --- a/src/TurboHTTP.AcceptanceTests/H3/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H3/ErrorHandlingSpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H3; diff --git a/src/TurboHTTP.AcceptanceTests/H3/ExpectContinueSpec.cs b/src/TurboHTTP.AcceptanceTests/H3/ExpectContinueSpec.cs index d11d07602..06cd9cf78 100644 --- a/src/TurboHTTP.AcceptanceTests/H3/ExpectContinueSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H3/ExpectContinueSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H3/RequestCompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/H3/RequestCompressionSpec.cs index be41a4cf7..594068cee 100644 --- a/src/TurboHTTP.AcceptanceTests/H3/RequestCompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H3/RequestCompressionSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/H3/ResilienceSpec.cs b/src/TurboHTTP.AcceptanceTests/H3/ResilienceSpec.cs index ebec10238..684ffb40b 100644 --- a/src/TurboHTTP.AcceptanceTests/H3/ResilienceSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/H3/ResilienceSpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Tests.Shared; namespace TurboHTTP.AcceptanceTests.H3; diff --git a/src/TurboHTTP.AcceptanceTests/ModuleInit.cs b/src/TurboHTTP.AcceptanceTests/ModuleInit.cs index f54afa8f7..7d3ba60f5 100644 --- a/src/TurboHTTP.AcceptanceTests/ModuleInit.cs +++ b/src/TurboHTTP.AcceptanceTests/ModuleInit.cs @@ -1,5 +1,5 @@ using System.Runtime.CompilerServices; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.AcceptanceTests; diff --git a/src/TurboHTTP.AcceptanceTests/Proxy/ProxyConnectSpec.cs b/src/TurboHTTP.AcceptanceTests/Proxy/ProxyConnectSpec.cs index 9dd49a6aa..5a7aa37ec 100644 --- a/src/TurboHTTP.AcceptanceTests/Proxy/ProxyConnectSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/Proxy/ProxyConnectSpec.cs @@ -2,17 +2,16 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.AcceptanceTests.Proxy; public sealed class ProxyConnectSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK) { @@ -20,19 +19,6 @@ private static byte[] BuildResponse(string body, HttpStatusCode status = HttpSta $"HTTP/1.1 {(int)status} {status}\r\nContent-Length: {Encoding.Latin1.GetByteCount(body)}\r\n\r\n{body}"); } - private static ConnectItem ToConnectItem(StreamAcquireItem acquire) - { - return new ConnectItem(new TcpOptions - { - Host = acquire.Key.Host, - Port = acquire.Key.Port, - UseProxy = true - }) - { - Key = acquire.Key - }; - } - private async Task<(HttpResponseMessage Response, string TunneledRequest)> SendViaTunnelAsync( HttpRequestMessage request, Func responseFactory) @@ -41,7 +27,6 @@ private static ConnectItem ToConnectItem(StreamAcquireItem acquire) var connectResponseConsumed = false; var tunnelFlow = Flow.Create() - .Select(item => item is StreamAcquireItem acquire ? ToConnectItem(acquire) : item) .Via(Flow.FromGraph(fake)) .Where(item => { diff --git a/src/TurboHTTP.AcceptanceTests/Proxy/ProxyRelaySpec.cs b/src/TurboHTTP.AcceptanceTests/Proxy/ProxyRelaySpec.cs index 78725307f..88e7ba8c8 100644 --- a/src/TurboHTTP.AcceptanceTests/Proxy/ProxyRelaySpec.cs +++ b/src/TurboHTTP.AcceptanceTests/Proxy/ProxyRelaySpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.Proxy; public sealed class ProxyRelaySpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK) { diff --git a/src/TurboHTTP.AcceptanceTests/Shared/FakeProxyStageSpec.cs b/src/TurboHTTP.AcceptanceTests/Shared/FakeProxyStageSpec.cs index 8625e7bec..4b9c5f8e1 100644 --- a/src/TurboHTTP.AcceptanceTests/Shared/FakeProxyStageSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/Shared/FakeProxyStageSpec.cs @@ -2,9 +2,9 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.AcceptanceTests.Shared; diff --git a/src/TurboHTTP.AcceptanceTests/Shared/ScriptedFakeConnectionStageSpec.cs b/src/TurboHTTP.AcceptanceTests/Shared/ScriptedFakeConnectionStageSpec.cs index d607beedc..68ae5bb9d 100644 --- a/src/TurboHTTP.AcceptanceTests/Shared/ScriptedFakeConnectionStageSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/Shared/ScriptedFakeConnectionStageSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.Shared; public sealed class ScriptedFakeConnectionStageSpec : EngineTestBase { private static Http10Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); [Fact(Timeout = 5000)] public async Task ScriptedFake_should_route_responses_by_request_index() diff --git a/src/TurboHTTP.AcceptanceTests/TLS/CompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/CompressionSpec.cs index 061a33708..6b4557b06 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/CompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/CompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -13,7 +13,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class CompressionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateDecompressingEngine() diff --git a/src/TurboHTTP.AcceptanceTests/TLS/ConnectionSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/ConnectionSpec.cs index 84353a85e..8dd1ace08 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/ConnectionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/ConnectionSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class ConnectionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK, string? extraHeaders = null) diff --git a/src/TurboHTTP.AcceptanceTests/TLS/ErrorHandlingSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/ErrorHandlingSpec.cs index c931a3f9b..cce8a6bc6 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/ErrorHandlingSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class ErrorHandlingSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] BuildResponse(string body, HttpStatusCode status = HttpStatusCode.OK, string? extraHeaders = null) diff --git a/src/TurboHTTP.AcceptanceTests/TLS/ExpectContinueSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/ExpectContinueSpec.cs index 0063db86b..32660b02d 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/ExpectContinueSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/ExpectContinueSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; @@ -13,7 +13,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class ExpectContinueSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateExpectContinueEngine() diff --git a/src/TurboHTTP.AcceptanceTests/TLS/IntegrationSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/IntegrationSpec.cs index 6b4e9cef1..5ce0d5286 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/IntegrationSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/IntegrationSpec.cs @@ -1,12 +1,9 @@ using System.Net; using System.Text; using System.Text.Json; -using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; using TurboHTTP.Protocol.Cookies; using TurboHTTP.Protocol.Semantics; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -14,29 +11,6 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class IntegrationSpec : AcceptanceTestBase { - private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); - - private async Task SendViaEngineAsync(HttpRequestMessage request, - Func? transform = null) - { - var fake = new ScriptedFakeConnectionStage((_, _) => - { - var body = "Hello World"; - var raw = $"HTTP/1.1 200 OK\r\nContent-Length: {body.Length}\r\n\r\n{body}"; - var bytes = Encoding.Latin1.GetBytes(raw); - return transform is not null ? transform(bytes) : bytes; - }); - var flow = Engine.CreateFlow().Join(Flow.FromGraph(fake)); - - var tcs = new TaskCompletionSource(); - _ = Source.Single(request) - .Via(flow) - .RunWith(Sink.ForEach(res => tcs.TrySetResult(res)), Materializer); - - return await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); - } - private async Task SendAsync(ResponseMap map, HttpRequestMessage request) { var fake = ResponseMapFake.Create(map); diff --git a/src/TurboHTTP.AcceptanceTests/TLS/RequestCompressionSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/RequestCompressionSpec.cs index 104962e27..124c38311 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/RequestCompressionSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/RequestCompressionSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; @@ -14,7 +14,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class RequestCompressionSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static byte[] MakePayload(int size) { diff --git a/src/TurboHTTP.AcceptanceTests/TLS/ResilienceSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/ResilienceSpec.cs index be2acf5f1..9fbe1321a 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/ResilienceSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/ResilienceSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -12,7 +12,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class ResilienceSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); private static BidiFlow CreateDecompressingEngine() diff --git a/src/TurboHTTP.AcceptanceTests/TLS/SmokeSpec.cs b/src/TurboHTTP.AcceptanceTests/TLS/SmokeSpec.cs index 406adac4f..8046cbce4 100644 --- a/src/TurboHTTP.AcceptanceTests/TLS/SmokeSpec.cs +++ b/src/TurboHTTP.AcceptanceTests/TLS/SmokeSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; @@ -11,7 +11,7 @@ namespace TurboHTTP.AcceptanceTests.TLS; public sealed class SmokeSpec : AcceptanceTestBase { private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + new(new TurboClientOptions()); [Fact(Timeout = 5000)] [Trait("RFC", "RFC9110-15.3")] diff --git a/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageReconnectSpec.cs b/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageReconnectSpec.cs index fafad2d02..8ab0bc66d 100644 --- a/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageReconnectSpec.cs +++ b/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageReconnectSpec.cs @@ -2,7 +2,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -29,7 +29,7 @@ private static NetworkBuffer MakeResponseBuffer(string raw) [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_reconnect_and_replay_request_on_connection_drop() { - var stage = new Http10ConnectionStage(maxReconnectAttempts: 3); + var stage = new Http10ConnectionStage(new TurboClientOptions { Http1 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -57,7 +57,9 @@ public async Task Http10ConnectionStage_should_reconnect_and_replay_request_on_c // Send a request appSub.SendNext(MakeRequest()); - // Consume StreamAcquireItem + NetworkBuffer + // Consume ConnectItem + StreamAcquireItem + NetworkBuffer + var item0 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + Assert.IsType(item0); var item1 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(item1); var item2 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -66,9 +68,10 @@ public async Task Http10ConnectionStage_should_reconnect_and_replay_request_on_c // Connection drops while request is in-flight serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); - // Stage must emit ReconnectItem (not fail or complete) + // Stage must emit ConnectItem with IsReconnect (not fail or complete) var reconnectRaw = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - var reconnect = Assert.IsType(reconnectRaw); + var reconnect = Assert.IsType(reconnectRaw); + Assert.True(reconnect.IsReconnect); // Simulate TcpConnectionStage reconnect success → sends ConnectedSignalItem serverSub.SendNext(new ConnectedSignalItem { Key = reconnect.Key }); @@ -90,7 +93,7 @@ public async Task Http10ConnectionStage_should_reconnect_and_replay_request_on_c [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_complete_stage_when_max_reconnect_attempts_exceeded() { - var stage = new Http10ConnectionStage(maxReconnectAttempts: 1); + var stage = new Http10ConnectionStage(new TurboClientOptions { Http1 = { MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -116,13 +119,15 @@ public async Task Http10ConnectionStage_should_complete_stage_when_max_reconnect resSub.Request(10); appSub.SendNext(MakeRequest()); + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // ConnectItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // StreamAcquireItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // NetworkBuffer // First drop → reconnect attempt 1 (hits max immediately) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); var reconnectRaw = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - Assert.IsType(reconnectRaw); + var reconnectItem = Assert.IsType(reconnectRaw); + Assert.True(reconnectItem.IsReconnect); // Reconnect fails → CloseSignalItem again (attempt 2 exceeds max of 1) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); @@ -135,7 +140,7 @@ public async Task Http10ConnectionStage_should_complete_stage_when_max_reconnect [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_not_reconnect_when_no_inflight_request_on_close() { - var stage = new Http10ConnectionStage(maxReconnectAttempts: 3); + var stage = new Http10ConnectionStage(new TurboClientOptions { Http1 = { MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageSpec.cs b/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageSpec.cs index c718156b0..14834c995 100644 --- a/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Http10/Http10ConnectionStageSpec.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -32,7 +32,7 @@ private static NetworkBuffer MakeResponseBuffer(string raw) [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_encode_request_and_emit_on_network_outlet() { - var stage = new Http10ConnectionStage(); + var stage = new Http10ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -67,6 +67,9 @@ public async Task Http10ConnectionStage_should_encode_request_and_emit_on_networ // Send a request appSubscription.SendNext(MakeRequest("/test")); + // ConnectItem emitted first when endpoint is known from the first request + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + // Should get StreamAcquireItem + NetworkBuffer on network outlet var item1 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(item1); @@ -82,7 +85,7 @@ public async Task Http10ConnectionStage_should_encode_request_and_emit_on_networ [Trait("RFC", "RFC1945-6")] public async Task Http10ConnectionStage_should_decode_response_and_correlate_with_request() { - var stage = new Http10ConnectionStage(); + var stage = new Http10ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -116,7 +119,8 @@ public async Task Http10ConnectionStage_should_decode_response_and_correlate_wit // Send request appSubscription.SendNext(MakeRequest("/hello")); - // Consume outbound items (StreamAcquire + NetworkBuffer) + // Consume outbound items (ConnectItem + StreamAcquire + NetworkBuffer) + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -135,7 +139,7 @@ public async Task Http10ConnectionStage_should_decode_response_and_correlate_wit [Trait("RFC", "RFC1945-7.2.2")] public async Task Http10ConnectionStage_should_emit_connection_reuse_close_for_http10() { - var stage = new Http10ConnectionStage(); + var stage = new Http10ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -169,7 +173,8 @@ public async Task Http10ConnectionStage_should_emit_connection_reuse_close_for_h // Send request + response appSubscription.SendNext(MakeRequest()); - // StreamAcquire + NetworkBuffer + // ConnectItem + StreamAcquire + NetworkBuffer + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -182,7 +187,7 @@ public async Task Http10ConnectionStage_should_emit_connection_reuse_close_for_h var reuseItem = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); var connectionReuse = Assert.IsType(reuseItem); // HTTP/1.0 default is close (RFC 1945) - Assert.False(connectionReuse.Decision.CanReuse); + Assert.False(connectionReuse.CanReuse); } @@ -190,7 +195,7 @@ public async Task Http10ConnectionStage_should_emit_connection_reuse_close_for_h [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_complete_stage_when_app_upstream_finishes_without_inflight() { - var stage = new Http10ConnectionStage(); + var stage = new Http10ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -232,7 +237,7 @@ public async Task Http10ConnectionStage_should_complete_stage_when_app_upstream_ [Trait("RFC", "RFC1945-4")] public async Task Http10ConnectionStage_should_complete_when_server_closes_and_no_response_pending() { - var stage = new Http10ConnectionStage(); + var stage = new Http10ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http10/Http10DecompressionPipelineSpec.cs b/src/TurboHTTP.StreamTests/Http10/Http10DecompressionPipelineSpec.cs index 30de52249..6600d7976 100644 --- a/src/TurboHTTP.StreamTests/Http10/Http10DecompressionPipelineSpec.cs +++ b/src/TurboHTTP.StreamTests/Http10/Http10DecompressionPipelineSpec.cs @@ -3,7 +3,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Features; using TurboHTTP.Tests.Shared; @@ -12,8 +12,7 @@ namespace TurboHTTP.StreamTests.Http10; public sealed class Http10DecompressionPipelineSpec : EngineTestBase { - private static readonly Http10Engine Engine = - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + private static readonly Http10Engine Engine = new(new TurboClientOptions()); private static BidiFlow CreateDecompressingEngine() diff --git a/src/TurboHTTP.StreamTests/Http10/Http10EngineEndToEndSpec.cs b/src/TurboHTTP.StreamTests/Http10/Http10EngineEndToEndSpec.cs index c94cb0e89..4ac0330a0 100644 --- a/src/TurboHTTP.StreamTests/Http10/Http10EngineEndToEndSpec.cs +++ b/src/TurboHTTP.StreamTests/Http10/Http10EngineEndToEndSpec.cs @@ -7,7 +7,7 @@ namespace TurboHTTP.StreamTests.Http10; public sealed class Http10EngineEndToEndSpec : EngineTestBase { - private static Http10Engine Engine => new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + private static Http10Engine Engine => new(new TurboClientOptions()); [Fact(Timeout = 10_000)] [Trait("RFC", "RFC1945-4.1")] diff --git a/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageReconnectSpec.cs b/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageReconnectSpec.cs index 06134a98e..9a41b566b 100644 --- a/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageReconnectSpec.cs +++ b/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageReconnectSpec.cs @@ -2,7 +2,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -29,7 +29,8 @@ private static NetworkBuffer MakeResponseBuffer(string raw) [Trait("RFC", "RFC9112-9.3")] public async Task Http11ConnectionStage_should_reconnect_and_replay_request_on_connection_drop() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 1, maxReconnectAttempts: 3); + var stage = new Http11ConnectionStage(new TurboClientOptions + { Http1 = { MaxPipelineDepth = 1, MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -57,7 +58,9 @@ public async Task Http11ConnectionStage_should_reconnect_and_replay_request_on_c // Send a request appSub.SendNext(MakeRequest()); - // Consume StreamAcquireItem + NetworkBuffer + // Consume ConnectItem + StreamAcquireItem + NetworkBuffer + var item0 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + Assert.IsType(item0); var item1 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(item1); var item2 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -66,9 +69,10 @@ public async Task Http11ConnectionStage_should_reconnect_and_replay_request_on_c // Connection drops while request is in-flight serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); - // Stage must emit ReconnectItem + // Stage must emit ConnectItem with IsReconnect var reconnectRaw = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - var reconnect = Assert.IsType(reconnectRaw); + var reconnect = Assert.IsType(reconnectRaw); + Assert.True(reconnect.IsReconnect); // Simulate reconnect success → sends ConnectedSignalItem serverSub.SendNext(new ConnectedSignalItem { Key = reconnect.Key }); @@ -90,7 +94,8 @@ public async Task Http11ConnectionStage_should_reconnect_and_replay_request_on_c [Trait("RFC", "RFC9112-9.3")] public async Task Http11ConnectionStage_should_complete_stage_when_max_reconnect_attempts_exceeded() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 1, maxReconnectAttempts: 1); + var stage = new Http11ConnectionStage(new TurboClientOptions + { Http1 = { MaxPipelineDepth = 1, MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -116,13 +121,15 @@ public async Task Http11ConnectionStage_should_complete_stage_when_max_reconnect resSub.Request(10); appSub.SendNext(MakeRequest()); + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // ConnectItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // StreamAcquireItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // NetworkBuffer // First drop → reconnect attempt 1 (hits max immediately) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); var reconnectRaw = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - Assert.IsType(reconnectRaw); + var reconnectItem2 = Assert.IsType(reconnectRaw); + Assert.True(reconnectItem2.IsReconnect); // Reconnect fails → CloseSignalItem again (attempt 2 exceeds max of 1) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); @@ -135,7 +142,8 @@ public async Task Http11ConnectionStage_should_complete_stage_when_max_reconnect [Trait("RFC", "RFC9112-9.3")] public async Task Http11ConnectionStage_should_not_reconnect_when_no_inflight_request_on_close() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 4, maxReconnectAttempts: 3); + var stage = new Http11ConnectionStage(new TurboClientOptions + { Http1 = { MaxPipelineDepth = 1, MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageSpec.cs b/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageSpec.cs index b0cd9b57d..b1a8edab3 100644 --- a/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Http11/Http11ConnectionStageSpec.cs @@ -2,7 +2,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; using SysEncoding = System.Text.Encoding; @@ -32,7 +32,7 @@ private static NetworkBuffer MakeResponseBuffer(string raw) [Trait("RFC", "RFC9112-6")] public async Task Http11ConnectionStage_should_encode_request_and_emit_on_network_outlet() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -68,6 +68,9 @@ public async Task Http11ConnectionStage_should_encode_request_and_emit_on_networ appSubscription.SendNext(MakeRequest("/test")); } + // ConnectItem emitted first when endpoint is known from the first request + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + // StreamAcquireItem + NetworkBuffer var item1 = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(item1); @@ -84,7 +87,7 @@ public async Task Http11ConnectionStage_should_encode_request_and_emit_on_networ [Trait("RFC", "RFC9112-6")] public async Task Http11ConnectionStage_should_decode_response_and_correlate_with_request() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -120,6 +123,7 @@ public async Task Http11ConnectionStage_should_decode_response_and_correlate_wit // Consume outbound await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); serverSubscription.SendNext(MakeResponseBuffer( "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello")); @@ -134,7 +138,7 @@ public async Task Http11ConnectionStage_should_decode_response_and_correlate_wit [Trait("RFC", "RFC9112-9.3")] public async Task Http11ConnectionStage_should_support_pipelining_multiple_requests() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 4); + var stage = new Http11ConnectionStage(new TurboClientOptions { Http1 = { MaxPipelineDepth = 4 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -167,12 +171,13 @@ public async Task Http11ConnectionStage_should_support_pipelining_multiple_reque // Send two requests (pipelined) appSubscription.SendNext(MakeRequest("/first")); - // StreamAcquire + NetworkBuffer for first request + // ConnectItem + StreamAcquire + NetworkBuffer for first request + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); appSubscription.SendNext(MakeRequest("/second")); - // StreamAcquire + NetworkBuffer for second request + // StreamAcquire + NetworkBuffer for second request (endpoint already known) await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -199,7 +204,7 @@ public async Task Http11ConnectionStage_should_support_pipelining_multiple_reque [Trait("RFC", "RFC9112-7")] public async Task Http11ConnectionStage_should_pipeline_requests_up_to_max_depth() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 3); + var stage = new Http11ConnectionStage(new TurboClientOptions { Http1 = { MaxPipelineDepth = 4 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -235,8 +240,9 @@ public async Task Http11ConnectionStage_should_pipeline_requests_up_to_max_depth appSubscription.SendNext(MakeRequest("/req2")); appSubscription.SendNext(MakeRequest("/req3")); - // Consume all 6 items (StreamAcquire + NetworkBuffer for each request) - for (var i = 0; i < 6; i++) + // Consume all 7 items: ConnectItem + StreamAcquire + NetworkBuffer for req1, + // StreamAcquire + NetworkBuffer for req2 and req3 + for (var i = 0; i < 7; i++) { await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); } @@ -265,7 +271,7 @@ public async Task Http11ConnectionStage_should_pipeline_requests_up_to_max_depth [Trait("RFC", "RFC9112-10.1")] public async Task Http11ConnectionStage_should_reduce_pipeline_depth_when_connection_close_received() { - var stage = new Http11ConnectionStage(maxPipelineDepth: 3); + var stage = new Http11ConnectionStage(new TurboClientOptions { Http1 = { MaxPipelineDepth = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -299,7 +305,8 @@ public async Task Http11ConnectionStage_should_reduce_pipeline_depth_when_connec // Send first request appSubscription.SendNext(MakeRequest("/req1")); - // Consume StreamAcquire + NetworkBuffer + // Consume ConnectItem + StreamAcquire + NetworkBuffer + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -315,7 +322,7 @@ public async Task Http11ConnectionStage_should_reduce_pipeline_depth_when_connec var reuseItem = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); var connectionReuse = Assert.IsType(reuseItem); // Connection: close means cannot reuse - Assert.False(connectionReuse.Decision.CanReuse); + Assert.False(connectionReuse.CanReuse); // After receiving Connection: close, the stage should reduce effective pipeline depth to 1. // Send a second request to verify it's still accepted @@ -337,7 +344,7 @@ public async Task Http11ConnectionStage_should_reduce_pipeline_depth_when_connec [Trait("RFC", "RFC9112-9.3")] public async Task Http11ConnectionStage_should_emit_connection_reuse_keep_alive_for_http11() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -373,6 +380,7 @@ public async Task Http11ConnectionStage_should_emit_connection_reuse_keep_alive_ // StreamAcquire + NetworkBuffer await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); serverSubscription.SendNext(MakeResponseBuffer( "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")); @@ -382,14 +390,14 @@ public async Task Http11ConnectionStage_should_emit_connection_reuse_keep_alive_ var reuseItem = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); var connectionReuse = Assert.IsType(reuseItem); // HTTP/1.1 default is keep-alive (RFC 9112) - Assert.True(connectionReuse.Decision.CanReuse); + Assert.True(connectionReuse.CanReuse); } [Fact(Timeout = 10_000)] [Trait("RFC", "RFC9112-7")] public async Task Http11ConnectionStage_should_handle_100_continue_response() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -422,7 +430,8 @@ public async Task Http11ConnectionStage_should_handle_100_continue_response() appSubscription.SendNext(MakeRequest("/upload")); - // Consume StreamAcquire + NetworkBuffer + // Consume ConnectItem + StreamAcquire + NetworkBuffer + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -441,7 +450,7 @@ public async Task Http11ConnectionStage_should_handle_100_continue_response() [Trait("RFC", "RFC9112-6")] public async Task Http11ConnectionStage_should_handle_connection_close_header() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -474,7 +483,8 @@ public async Task Http11ConnectionStage_should_handle_connection_close_header() appSubscription.SendNext(MakeRequest("/close")); - // Consume StreamAcquire + NetworkBuffer + // Consume ConnectItem + StreamAcquire + NetworkBuffer + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -488,14 +498,14 @@ public async Task Http11ConnectionStage_should_handle_connection_close_header() // ConnectionReuseItem should indicate cannot reuse var reuseItem = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); var connectionReuse = Assert.IsType(reuseItem); - Assert.False(connectionReuse.Decision.CanReuse); + Assert.False(connectionReuse.CanReuse); } [Fact(Timeout = 10_000)] [Trait("RFC", "RFC9112-6")] public async Task Http11ConnectionStage_should_complete_when_app_upstream_finishes_and_no_inflight() { - var stage = new Http11ConnectionStage(); + var stage = new Http11ConnectionStage(new TurboClientOptions()); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http11/Http11EngineEndToEndSpec.cs b/src/TurboHTTP.StreamTests/Http11/Http11EngineEndToEndSpec.cs index 407dc9107..1915e5994 100644 --- a/src/TurboHTTP.StreamTests/Http11/Http11EngineEndToEndSpec.cs +++ b/src/TurboHTTP.StreamTests/Http11/Http11EngineEndToEndSpec.cs @@ -7,8 +7,7 @@ namespace TurboHTTP.StreamTests.Http11; public sealed class Http11EngineEndToEndSpec : EngineTestBase { - private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + private static Http11Engine Engine => new(new TurboClientOptions()); private static byte[] Ok200(string body) => TextEncoding.Latin1.GetBytes($"HTTP/1.1 200 OK\r\nContent-Length: {body.Length}\r\n\r\n{body}"); diff --git a/src/TurboHTTP.StreamTests/Http11/Http11KeepAliveCloseSpec.cs b/src/TurboHTTP.StreamTests/Http11/Http11KeepAliveCloseSpec.cs index 772127b85..e99e5a233 100644 --- a/src/TurboHTTP.StreamTests/Http11/Http11KeepAliveCloseSpec.cs +++ b/src/TurboHTTP.StreamTests/Http11/Http11KeepAliveCloseSpec.cs @@ -6,8 +6,7 @@ namespace TurboHTTP.StreamTests.Http11; public sealed class Http11KeepAliveCloseSpec : EngineTestBase { - private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + private static Http11Engine Engine => new(new TurboClientOptions()); [Fact(Timeout = 10_000)] [Trait("RFC", "RFC9112-9.6")] diff --git a/src/TurboHTTP.StreamTests/Http11/Http11ResponseCorrelationSpec.cs b/src/TurboHTTP.StreamTests/Http11/Http11ResponseCorrelationSpec.cs index 76d2c5dbf..323df92b9 100644 --- a/src/TurboHTTP.StreamTests/Http11/Http11ResponseCorrelationSpec.cs +++ b/src/TurboHTTP.StreamTests/Http11/Http11ResponseCorrelationSpec.cs @@ -8,8 +8,7 @@ public sealed class Http11ResponseCorrelationSpec : EngineTestBase { private static readonly Func Ok200 = () => "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"u8.ToArray(); - private static Http11Engine Engine => - new(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); + private static Http11Engine Engine => new(new TurboClientOptions()); [Fact(Timeout = 10_000)] [Trait("RFC", "RFC9112-9.3")] @@ -60,14 +59,11 @@ public async Task Http11ResponseCorrelation_should_use_exact_same_reference_when [Trait("RFC", "RFC9112-9.3")] public async Task Http11ResponseCorrelation_should_preserve_correlation_when_fake_tcp_used() { - var engine = - new Http11Engine(new Http1EngineOptions(16, 6, 3, 64 * 1024, 64, 1024 * 1024, TimeSpan.FromSeconds(2))); - var request1 = new HttpRequestMessage(HttpMethod.Get, "http://a.test/one"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://a.test/two"); var request3 = new HttpRequestMessage(HttpMethod.Delete, "http://a.test/three"); - var (responses, _) = await SendManyAsync(engine.CreateFlow(), + var (responses, _) = await SendManyAsync(Engine.CreateFlow(), [request1, request2, request3], Ok200, 3); Assert.Equal(3, responses.Count); diff --git a/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageReconnectSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageReconnectSpec.cs index dc729d19b..5c15880c0 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageReconnectSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageReconnectSpec.cs @@ -1,7 +1,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -19,7 +19,7 @@ private static HttpRequestMessage MakeRequest(string path = "/") => [Trait("RFC", "RFC9113-6.8")] public async Task Http20ConnectionStage_should_emit_reconnect_item_on_abrupt_close_with_inflight() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -48,8 +48,10 @@ public async Task Http20ConnectionStage_should_emit_reconnect_item_on_abrupt_clo var preface = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(preface); - // Send a request + // Send a request — first request also emits ConnectItem before StreamAcquireItem appSub.SendNext(MakeRequest()); + var connectItem = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + Assert.IsType(connectItem); var acquire = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(acquire); var headers = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); @@ -58,16 +60,17 @@ public async Task Http20ConnectionStage_should_emit_reconnect_item_on_abrupt_clo // Abrupt TCP close with no GOAWAY — in-flight request exists serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); - // Stage must emit ReconnectItem instead of failing + // Stage must emit ConnectItem with IsReconnect instead of failing var reconnect = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - Assert.IsType(reconnect); + var reconnectItem = Assert.IsType(reconnect); + Assert.True(reconnectItem.IsReconnect); } [Fact(Timeout = 10000)] [Trait("RFC", "RFC9113-6.8")] public async Task Http20ConnectionStage_should_fail_when_max_reconnect_attempts_exceeded() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 1 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -95,13 +98,15 @@ public async Task Http20ConnectionStage_should_fail_when_max_reconnect_attempts_ await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // preface appSub.SendNext(MakeRequest()); + await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // ConnectItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // StreamAcquireItem await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); // HEADERS frame // First drop → reconnect attempt 1 (hits max immediately) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); var reconnect = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); - Assert.IsType(reconnect); + var reconnectItem2 = Assert.IsType(reconnect); + Assert.True(reconnectItem2.IsReconnect); // Reconnect fails → CloseSignalItem again (attempt 2 exceeds max of 1) serverSub.SendNext(new CloseSignalItem(TlsCloseKind.AbruptClose)); @@ -114,7 +119,7 @@ public async Task Http20ConnectionStage_should_fail_when_max_reconnect_attempts_ [Trait("RFC", "RFC9113-6.8")] public async Task Http20ConnectionStage_should_complete_normally_on_close_with_no_inflight() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 1 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageSpec.cs index a8591544d..e51de0d35 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http20ConnectionStageSpec.cs @@ -2,7 +2,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -31,7 +31,8 @@ private static NetworkBuffer MakeResponseBuffer(string raw) [Trait("RFC", "RFC9113-3.2")] public async Task Http20ConnectionStage_should_emit_preface_on_first_network_pull() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions + { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -74,7 +75,8 @@ public async Task Http20ConnectionStage_should_emit_preface_on_first_network_pul [Trait("RFC", "RFC9113-6")] public async Task Http20ConnectionStage_should_encode_request_as_headers_frame() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions + { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -112,7 +114,10 @@ public async Task Http20ConnectionStage_should_encode_request_as_headers_frame() // Send request appSubscription.SendNext(MakeRequest("/test")); - // Should emit StreamAcquireItem + HEADERS frame (as NetworkBuffer) + // First request: ConnectItem (transport connect) + StreamAcquireItem + HEADERS frame (as NetworkBuffer) + var connect = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); + Assert.IsType(connect); + var acquire = await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken); Assert.IsType(acquire); @@ -124,7 +129,7 @@ public async Task Http20ConnectionStage_should_encode_request_as_headers_frame() [Trait("RFC", "RFC9113-6.2")] public async Task Http20ConnectionStage_should_support_stream_multiplexing() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -176,7 +181,7 @@ public async Task Http20ConnectionStage_should_support_stream_multiplexing() [Trait("RFC", "RFC9113-3.1")] public async Task Http20ConnectionStage_should_handle_settings_frame() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -222,7 +227,7 @@ public async Task Http20ConnectionStage_should_handle_settings_frame() [Trait("RFC", "RFC9113-6.8")] public async Task Http20ConnectionStage_should_complete_on_goaway_with_no_inflight() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -267,7 +272,7 @@ public async Task Http20ConnectionStage_should_complete_on_goaway_with_no_inflig [Trait("RFC", "RFC9113-6")] public async Task Http20ConnectionStage_should_complete_when_app_upstream_finishes_with_no_inflight() { - var stage = new Http20ConnectionStage(new Http2Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions { Http2 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionBackpressureSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionBackpressureSpec.cs index 420d9609a..05e365e12 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionBackpressureSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionBackpressureSpec.cs @@ -1,7 +1,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -27,8 +27,8 @@ public sealed class Http2ConnectionBackpressureSpec : StreamTestBase Source.Queue(16, OverflowStrategy.Backpressure), (b, reqSrc) => { - var stage = b.Add(new Http20ConnectionStage( - new Http2Options { MaxConcurrentStreams = maxConcurrentStreams }.ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { MaxConcurrentStreams = maxConcurrentStreams } })); var srvSrc = b.Add(Source.FromPublisher(serverProbe)); b.From(srvSrc).To(stage.InServer); @@ -67,7 +67,13 @@ private static async Task FillStreamsAsync(ISourceQueueWithComplete (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage( - new Http2Options { InitialConnectionWindowSize = initialWindowSize }.ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions() + { Http2 = { InitialConnectionWindowSize = initialWindowSize } })); var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); var requestSource = b.Add(Source.Never()); diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionFlowControlSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionFlowControlSpec.cs index f89439ad9..510ba7b12 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionFlowControlSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionFlowControlSpec.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -13,7 +13,7 @@ public sealed class Http2ConnectionFlowControlSpec : StreamTestBase { private Task<(IReadOnlyList Downstream, IReadOnlyList ServerBound)> RunAsync( params Http2Frame[] serverFrames) - => RunFlowAsync(new Http20ConnectionStage(new Http2Options().ToEngineOptions()), serverFrames); + => RunFlowAsync(new Http20ConnectionStage(new TurboClientOptions()), serverFrames); private async Task<(IReadOnlyList Downstream, IReadOnlyList ServerBound)> RunFlowAsync( @@ -82,8 +82,8 @@ public async Task Http2ConnectionFlowControl_should_send_connection_window_updat { // Explicit 65535-byte window → threshold = max(8192, 65535/4) = 16384. // Sending exactly 16384 bytes crosses the threshold in a single DATA frame. - var stage = new Http20ConnectionStage( - new Http2Options { InitialConnectionWindowSize = 65535 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } }); var data = new DataFrame(streamId: 1, data: new byte[16384], endStream: true); var (_, serverBound) = await RunFlowAsync(stage, data); @@ -118,8 +118,8 @@ public async Task Http2ConnectionFlowControl_should_send_both_window_updates_whe { // Explicit 65535-byte window → threshold = 16384. Exactly 16384 bytes on a single // DATA frame crosses both the connection and stream thresholds simultaneously. - var stage = new Http20ConnectionStage( - new Http2Options { InitialConnectionWindowSize = 65535 }.ToEngineOptions()); + var stage = new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } }); var data = new DataFrame(streamId: 3, data: new byte[16384], endStream: true); var (_, serverBound) = await RunFlowAsync(stage, data); @@ -145,7 +145,8 @@ public async Task Http2ConnectionFlowControl_should_survive_and_log_when_connect (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs([data]))); var requestSource = b.Add(Source.Never()); @@ -158,7 +159,7 @@ public async Task Http2ConnectionFlowControl_should_survive_and_log_when_connect })); var mat = graph.Run(Materializer); - var (downstreamTask, networkTask) = (mat.Item1, mat.Item2); + var (downstreamTask, networkTask) = (mat.m1, mat.m2); await Task.Delay(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); @@ -182,7 +183,8 @@ public async Task Http2ConnectionFlowControl_should_survive_and_log_when_stream_ (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs([data]))); var requestSource = b.Add(Source.Never()); @@ -219,7 +221,8 @@ public async Task Http2ConnectionFlowControl_should_survive_and_log_when_outboun (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.Never()); var requestSource = b.Add(Source.Single(request)); @@ -254,7 +257,8 @@ public async Task Http2ConnectionFlowControl_should_forward_data_when_outbound_d GraphDsl.Create(networkSink, (b, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.Never()); var requestSource = b.Add(Source.Single(request)); var ignoreSink = @@ -291,7 +295,8 @@ public async Task Http2ConnectionFlowControl_should_increment_connection_window_ GraphDsl.Create(networkSink, (b, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); // Server sends WINDOW_UPDATEs immediately, then a harmless SETTINGS ACK // after a delay to keep InServer alive until the request has been processed. diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionGoAwaySpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionGoAwaySpec.cs index 5363b68e7..761ff0ab3 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionGoAwaySpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionGoAwaySpec.cs @@ -1,6 +1,6 @@ using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -21,7 +21,8 @@ public sealed class Http2ConnectionGoAwaySpec : StreamTestBase (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); var requestSource = b.Add(Source.Never()); @@ -69,7 +70,8 @@ public async Task Http2ConnectionGoAway_should_drop_new_requests_without_failing (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); // Server sends GOAWAY then stays open (never finishes) var serverSource = b.Add( diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionPingSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionPingSpec.cs index effceeba0..0b6e1baaf 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionPingSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionPingSpec.cs @@ -1,6 +1,6 @@ using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -22,7 +22,8 @@ public sealed class Http2ConnectionPingSpec : StreamTestBase (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); var requestSource = b.Add(Source.Never()); diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionSettingsSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionSettingsSpec.cs index c38905024..0fb84b54e 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionSettingsSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionSettingsSpec.cs @@ -1,6 +1,6 @@ using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -22,7 +22,8 @@ public sealed class Http2ConnectionSettingsSpec : StreamTestBase (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); var requestSource = b.Add(Source.Never()); @@ -101,7 +102,8 @@ public async Task Http2ConnectionSettings_should_survive_when_inbound_data_excee (m1, m2) => (m1, m2), (b, dsSink, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs([data]))); var requestSource = b.Add(Source.Never()); diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionStreamAcquireSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionStreamAcquireSpec.cs index 4edff76fe..c9068f7ae 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionStreamAcquireSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionStreamAcquireSpec.cs @@ -2,7 +2,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -22,7 +22,8 @@ public sealed class Http2ConnectionStreamAcquireSpec : StreamTestBase GraphDsl.Create(networkSink, (b, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); // A SETTINGS ACK on InServer is harmless (no ACK reply) and lets // the inlet complete, which tears down the stage via the default @@ -60,7 +61,8 @@ public sealed class Http2ConnectionStreamAcquireSpec : StreamTestBase GraphDsl.Create(networkSink, (b, nwSink) => { - var stage = b.Add(new Http20ConnectionStage(new Http2Options().ToEngineOptions())); + var stage = b.Add(new Http20ConnectionStage(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } })); var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); var requestSource = b.Add( @@ -93,7 +95,7 @@ public async Task Http2ConnectionStreamAcquire_should_emit_stream_acquire_item_w var (_, signals) = await RunWithRequestsAsync(request); - var signal = Assert.Single(signals); + var signal = Assert.Single(signals.OfType()); Assert.IsType(signal); } @@ -110,7 +112,8 @@ public async Task Http2ConnectionStreamAcquire_should_not_emit_signal_when_data_ var (_, signals) = await RunWithRequestsAsync(request); - Assert.Single(signals); + // Only the HeadersFrame triggers a StreamAcquireItem; ConnectItem and DATA frame must not add one. + Assert.Single(signals.OfType()); } [Fact(Timeout = 10_000)] @@ -132,7 +135,7 @@ public async Task Http2ConnectionStreamAcquire_should_include_correct_key_in_str var (_, signals) = await RunWithRequestsAsync(request); - var signal = Assert.Single(signals); + var signal = Assert.Single(signals.OfType()); var acquire = Assert.IsType(signal); Assert.Equal(endpoint, acquire.Key); } @@ -145,7 +148,7 @@ public async Task Http2ConnectionStreamAcquire_should_use_default_key_in_stream_ var (_, signals) = await RunWithRequestsAsync(request); - var signal = Assert.Single(signals); + var signal = Assert.Single(signals.OfType()); var acquire = Assert.IsType(signal); Assert.Equal(RequestEndpoint.Default, acquire.Key); } @@ -170,9 +173,10 @@ public async Task Http2ConnectionStreamAcquire_should_capture_endpoint_once_and_ var (_, signals) = await RunWithRequestsAsync(req1, req2); - Assert.Equal(2, signals.Count); - var acquire1 = Assert.IsType(signals[0]); - var acquire2 = Assert.IsType(signals[1]); + var acquires = signals.OfType().ToList(); + Assert.Equal(2, acquires.Count); + var acquire1 = Assert.IsType(acquires[0]); + var acquire2 = Assert.IsType(acquires[1]); Assert.Equal(endpoint, acquire1.Key); Assert.Equal(endpoint, acquire2.Key); } diff --git a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionTestHelper.cs b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionTestHelper.cs index e40cc9add..c9e8ce22c 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2ConnectionTestHelper.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2ConnectionTestHelper.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; namespace TurboHTTP.StreamTests.Http2; diff --git a/src/TurboHTTP.StreamTests/Http2/Http2EngineEndToEndSpec.cs b/src/TurboHTTP.StreamTests/Http2/Http2EngineEndToEndSpec.cs index 70df2db94..5901fe165 100644 --- a/src/TurboHTTP.StreamTests/Http2/Http2EngineEndToEndSpec.cs +++ b/src/TurboHTTP.StreamTests/Http2/Http2EngineEndToEndSpec.cs @@ -3,18 +3,19 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; using TurboHTTP.Protocol.Http2; using TurboHTTP.Protocol.Http2.Hpack; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.StreamTests.Http2; public sealed class Http2EngineEndToEndSpec : EngineTestBase { - private static Http20Engine Engine => new(new Http2Options().ToEngineOptions()); + private static Http20Engine Engine => + new(new TurboClientOptions { Http2 = { InitialConnectionWindowSize = 65535 } }); private readonly HpackEncoder _hpack = new(useHuffman: false); private static readonly int[] Expected = [1, 3, 5]; @@ -205,7 +206,7 @@ public async Task Http2Engine_should_produce_three_responses_with_stream_ids_1_3 public async Task Http2Engine_should_produce_max_concurrent_streams_signal_when_settings_max_concurrent_streams_received() { - var engine = new Http20Engine(new Http2Options().ToEngineOptions()); + var engine = new Http20Engine(new TurboClientOptions { Http2 = { InitialConnectionWindowSize = 65535 } }); var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/signal-test") { @@ -259,7 +260,8 @@ public async Task [Trait("RFC", "RFC9113-3.4")] public async Task Http2Engine_should_emit_connection_preface_when_first_connect_item_arrives() { - var engine = new Http20Engine(new Http2Options().ToEngineOptions()); + var engine = new Http20Engine(new TurboClientOptions + { Http2 = { InitialConnectionWindowSize = 65535 } }); var bidiFlow = engine.CreateFlow(); var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/preface-test") diff --git a/src/TurboHTTP.StreamTests/Http3/Http30ConnectionConcurrencySpec.cs b/src/TurboHTTP.StreamTests/Http3/Http30ConnectionConcurrencySpec.cs index f162ab5ce..aa7e51ec2 100644 --- a/src/TurboHTTP.StreamTests/Http3/Http30ConnectionConcurrencySpec.cs +++ b/src/TurboHTTP.StreamTests/Http3/Http30ConnectionConcurrencySpec.cs @@ -1,10 +1,9 @@ using System.Net; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; using TurboHTTP.Protocol.Http3.Qpack; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -12,8 +11,6 @@ namespace TurboHTTP.StreamTests.Http3; public sealed class Http30ConnectionConcurrencySpec : StreamTestBase { - private static Http3EngineOptions DefaultOptions => new Http3Options().ToEngineOptions(); - private readonly QpackEncoder _qpack = new(maxTableCapacity: 0); private static readonly string[] Expected = ["/alpha", "/beta", "/gamma"]; @@ -27,29 +24,27 @@ private IEnumerable BuildResponseSequence(params long[] streamIds) var headersBytes = new Http3HeadersFrame( EncodeResponseHeaders((":status", "200"))).Serialize(); - var buf = Http3NetworkBuffer.Rent(headersBytes.Length); + var buf = RoutedNetworkBuffer.Rent(headersBytes.Length); headersBytes.AsSpan().CopyTo(buf.FullMemory.Span); buf.Length = headersBytes.Length; - buf.StreamType = Http3StreamType.Request; buf.StreamId = streamId; - yield return buf; yield return new QuicCloseItem(QuicCloseKind.RequestStreamComplete, streamId); } } - private static Http3NetworkBuffer BuildControlSettings() + private static RoutedNetworkBuffer BuildControlSettings() { var settingsBytes = new Http3SettingsFrame([]).Serialize(); - var buf = Http3NetworkBuffer.Rent(settingsBytes.Length); + var buf = RoutedNetworkBuffer.Rent(settingsBytes.Length); settingsBytes.AsSpan().CopyTo(buf.FullMemory.Span); buf.Length = settingsBytes.Length; - buf.StreamType = Http3StreamType.Control; + buf.StreamTypeValue = (long)StreamType.Control; return buf; } private async Task<(IReadOnlyList OutboundItems, IReadOnlyList Responses)> - RunConcurrentAsync(HttpRequestMessage[] requests, long[] responseStreamIds, Http3EngineOptions? options = null) + RunConcurrentAsync(HttpRequestMessage[] requests, long[] responseStreamIds, Http3Options? options = null) { var networkSink = Sink.Seq(); var responseSink = Sink.Seq(); @@ -61,7 +56,8 @@ private static Http3NetworkBuffer BuildControlSettings() GraphDsl.Create(networkSink, responseSink, (nw, resp) => (nw, resp), (b, nwSink, respSink) => { - var stage = b.Add(new Http30ConnectionStage(options ?? DefaultOptions)); + var stage = b.Add(new Http30ConnectionStage(new TurboClientOptions + { Http3 = options ?? new Http3Options() })); // Server responses arrive after a short delay to allow request encoding first var serverSource = b.Add( @@ -93,10 +89,10 @@ private static List ExtractRequestStreamIds(IReadOnlyList ite var result = new List(); foreach (var item in items) { - if (item is Http3NetworkBuffer { StreamType: Http3StreamType.Request, StreamId: >= 0 } tagged - && seen.Add(tagged.StreamId)) + if (item is RoutedNetworkBuffer { StreamTypeValue: null, StreamId: not null } tagged + && seen.Add(tagged.StreamId.Value)) { - result.Add(tagged.StreamId); + result.Add(tagged.StreamId.Value); } } diff --git a/src/TurboHTTP.StreamTests/Http3/Http30ConnectionStageSpec.cs b/src/TurboHTTP.StreamTests/Http3/Http30ConnectionStageSpec.cs index 1557b38f7..746ca70d8 100644 --- a/src/TurboHTTP.StreamTests/Http3/Http30ConnectionStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Http3/Http30ConnectionStageSpec.cs @@ -1,8 +1,7 @@ -using System.Text; using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.TestKit; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Tests.Shared; @@ -22,7 +21,7 @@ private static HttpRequestMessage MakeRequest(string path = "/") [Trait("RFC", "RFC9114-4")] public async Task Http30ConnectionStage_should_route_to_correct_quic_stream() { - var stage = new Http30ConnectionStage(new Http3Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http30ConnectionStage(new TurboClientOptions { Http3 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -71,12 +70,14 @@ public async Task Http30ConnectionStage_should_route_to_correct_quic_stream() [Trait("RFC", "RFC9114-5.2")] public async Task Http30ConnectionStage_should_handle_idle_timeout() { - var stage = new Http30ConnectionStage( - new Http3Options + var stage = new Http30ConnectionStage(new TurboClientOptions + { + Http3 = { MaxReconnectAttempts = 3, IdleTimeout = TimeSpan.FromMilliseconds(100) - }.ToEngineOptions()); + } + }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); @@ -118,7 +119,7 @@ public async Task Http30ConnectionStage_should_handle_idle_timeout() [Trait("RFC", "RFC9114-3")] public async Task Http30ConnectionStage_should_complete_when_app_upstream_finishes_with_no_inflight() { - var stage = new Http30ConnectionStage(new Http3Options { MaxReconnectAttempts = 3 }.ToEngineOptions()); + var stage = new Http30ConnectionStage(new TurboClientOptions { Http3 = { MaxReconnectAttempts = 3 } }); var appProbe = this.CreateManualPublisherProbe(); var serverProbe = this.CreateManualPublisherProbe(); diff --git a/src/TurboHTTP.StreamTests/Http3/Http30EngineEndToEndSpec.cs b/src/TurboHTTP.StreamTests/Http3/Http30EngineEndToEndSpec.cs index a67d95210..feb22f260 100644 --- a/src/TurboHTTP.StreamTests/Http3/Http30EngineEndToEndSpec.cs +++ b/src/TurboHTTP.StreamTests/Http3/Http30EngineEndToEndSpec.cs @@ -1,7 +1,6 @@ using System.IO.Compression; using System.Net; using System.Text; -using TurboHTTP.Internal; using TurboHTTP.Protocol.Http3; using TurboHTTP.Protocol.Http3.Qpack; using TurboHTTP.Streams; @@ -11,7 +10,7 @@ namespace TurboHTTP.StreamTests.Http3; public sealed class Http30EngineEndToEndSpec : EngineTestBase { - private static Http30Engine Engine => new(new Http3Options().ToEngineOptions()); + private static Http30Engine Engine => new(new TurboClientOptions()); private readonly QpackEncoder _qpack = new(maxTableCapacity: 0); diff --git a/src/TurboHTTP.StreamTests/ModuleInit.cs b/src/TurboHTTP.StreamTests/ModuleInit.cs index 1a2d4281a..b10c3db5f 100644 --- a/src/TurboHTTP.StreamTests/ModuleInit.cs +++ b/src/TurboHTTP.StreamTests/ModuleInit.cs @@ -1,5 +1,5 @@ using System.Runtime.CompilerServices; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.StreamTests; diff --git a/src/TurboHTTP.StreamTests/Streams/ConnectionStageSpec.cs b/src/TurboHTTP.StreamTests/Streams/ConnectionStageSpec.cs index ff228e71f..d802f050c 100644 --- a/src/TurboHTTP.StreamTests/Streams/ConnectionStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/ConnectionStageSpec.cs @@ -4,11 +4,10 @@ using Akka.Actor; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; using TurboHTTP.Protocol.Http11; using TurboHTTP.Tests.Shared; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Tcp; namespace TurboHTTP.StreamTests.Streams; @@ -79,7 +78,7 @@ private static NetworkBuffer MakeData(byte value, int length = 4) var lease = new ConnectionLease(handle, state); var tracker = new ReleaseTracker(); var actor = Sys.ActorOf(StubConnectionManagerActor.Props(lease, tracker)); - var stageFlow = Flow.FromGraph(new TcpConnectionStage(actor, new TurboClientOptions())); + var stageFlow = Flow.FromGraph(new TcpConnectionStage(actor)); return (stageFlow, tracker, lease, state.OutboundReader, state.InboundWriter); } @@ -92,7 +91,7 @@ public async Task ConnectionStage_should_trigger_acquire_async_when_connect_item var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (queue, _) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -121,7 +120,7 @@ public async Task ConnectionStage_should_reach_outlet_when_inbound_data_written_ var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, resultTask) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -156,7 +155,7 @@ public async Task ConnectionStage_should_write_to_outbound_channel_when_data_ite var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var data = MakeData(0xCD, 8); @@ -187,7 +186,7 @@ public async Task ConnectionStage_should_complete_round_trip_when_outbound_writt var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, resultTask) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -237,7 +236,7 @@ public async Task ConnectionStage_should_release_with_no_reuse_when_connection_r var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, _) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -249,7 +248,7 @@ public async Task ConnectionStage_should_release_with_no_reuse_when_connection_r await Task.Delay(300, TestContext.Current.CancellationToken); var decision = ConnectionReuseDecision.Close("Connection: close"); - var reuseItem = new ConnectionReuseItem(decision) { Key = TestKey }; + var reuseItem = new ConnectionReuseItem(decision.CanReuse) { Key = TestKey }; await inputQueue.OfferAsync(reuseItem); AwaitCondition(() => tracker.Released, TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken); @@ -269,7 +268,7 @@ public async Task ConnectionStage_should_release_with_can_reuse_when_connection_ var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, _) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -281,7 +280,7 @@ public async Task ConnectionStage_should_release_with_can_reuse_when_connection_ await Task.Delay(300, TestContext.Current.CancellationToken); var decision = ConnectionReuseDecision.KeepAlive("HTTP/1.1 persistent"); - var reuseItem = new ConnectionReuseItem(decision) { Key = TestKey }; + var reuseItem = new ConnectionReuseItem(decision.CanReuse) { Key = TestKey }; await inputQueue.OfferAsync(reuseItem); await Task.Delay(300, TestContext.Current.CancellationToken); @@ -303,7 +302,7 @@ public async Task var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, _) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -330,7 +329,7 @@ public async Task ConnectionStage_should_mark_lease_busy_when_stream_acquire_ite var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, _) = Source.Queue(4, OverflowStrategy.Backpressure) @@ -357,13 +356,17 @@ public async Task ConnectionStage_should_survive_and_continue_when_data_item_arr // Actor that never returns a lease var tracker = new ReleaseTracker(); var neverActor = Sys.ActorOf(StubConnectionManagerActor.Props(null, tracker)); - var stageFlow = Flow.FromGraph(new TcpConnectionStage(neverActor, new TurboClientOptions())); + var stageFlow = Flow.FromGraph(new TcpConnectionStage(neverActor)); var (inputQueue, outputTask) = Source.Queue(8, OverflowStrategy.Backpressure) .Via(stageFlow) .ToMaterialized(Sink.Seq(), Keep.Both) .Run(Materializer); + var options = new TcpOptions { Host = TestKey.Host, Port = TestKey.Port }; + var connectItem = new ConnectItem(options) { Key = TestKey }; + await inputQueue.OfferAsync(connectItem); + var data = MakeData(0xFF); await inputQueue.OfferAsync(data); @@ -389,12 +392,12 @@ public async Task var tracker = new ReleaseTracker(); var actor = Sys.ActorOf(StubConnectionManagerActor.Props(lease, tracker)); - var stageFlow = Flow.FromGraph(new TcpConnectionStage(actor, new TurboClientOptions())); + var stageFlow = Flow.FromGraph(new TcpConnectionStage(actor)); var options = new TcpOptions { Host = "localhost", Port = 8080 }; var connectItem = new ConnectItem(options) { Key = new RequestEndpoint - { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } + { Host = "localhost", Port = 8080, Scheme = "Https", Version = HttpVersion.Unknown } }; var (inputQueue, resultTask) = Source.Queue(4, OverflowStrategy.Backpressure) diff --git a/src/TurboHTTP.StreamTests/Streams/DelegateTransportFactory.cs b/src/TurboHTTP.StreamTests/Streams/DelegateTransportFactory.cs index 89beb15e8..c81b4735a 100644 --- a/src/TurboHTTP.StreamTests/Streams/DelegateTransportFactory.cs +++ b/src/TurboHTTP.StreamTests/Streams/DelegateTransportFactory.cs @@ -1,7 +1,6 @@ using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; -using TurboHTTP.Streams; +using Servus.Akka.IO; namespace TurboHTTP.StreamTests.Streams; diff --git a/src/TurboHTTP.StreamTests/Streams/EngineBidiFlowCompositionSpec.cs b/src/TurboHTTP.StreamTests/Streams/EngineBidiFlowCompositionSpec.cs index d9a0f86fd..6c4cfe124 100644 --- a/src/TurboHTTP.StreamTests/Streams/EngineBidiFlowCompositionSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/EngineBidiFlowCompositionSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Caching; using TurboHTTP.Protocol.Cookies; using TurboHTTP.Protocol.Semantics; diff --git a/src/TurboHTTP.StreamTests/Streams/EnginePipelineDescriptorSpec.cs b/src/TurboHTTP.StreamTests/Streams/EnginePipelineDescriptorSpec.cs index 4c3f389cc..c2c3989f1 100644 --- a/src/TurboHTTP.StreamTests/Streams/EnginePipelineDescriptorSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/EnginePipelineDescriptorSpec.cs @@ -2,7 +2,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Cookies; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; diff --git a/src/TurboHTTP.StreamTests/Streams/FeedbackBufferOptimizationSpec.cs b/src/TurboHTTP.StreamTests/Streams/FeedbackBufferOptimizationSpec.cs index a4e040430..6ba1454a0 100644 --- a/src/TurboHTTP.StreamTests/Streams/FeedbackBufferOptimizationSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/FeedbackBufferOptimizationSpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.StreamTests/Streams/GroupByEndpointFanOutSpec.cs b/src/TurboHTTP.StreamTests/Streams/GroupByEndpointFanOutSpec.cs index eafe79e64..ae9d874df 100644 --- a/src/TurboHTTP.StreamTests/Streams/GroupByEndpointFanOutSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/GroupByEndpointFanOutSpec.cs @@ -2,7 +2,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Internal; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.StreamTests/Streams/GroupByHostKeyQueueSizeSpec.cs b/src/TurboHTTP.StreamTests/Streams/GroupByHostKeyQueueSizeSpec.cs index 51686676e..90dd58580 100644 --- a/src/TurboHTTP.StreamTests/Streams/GroupByHostKeyQueueSizeSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/GroupByHostKeyQueueSizeSpec.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Streams.Stages.Internal; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.StreamTests/Streams/HostKeySubFlowSpec.cs b/src/TurboHTTP.StreamTests/Streams/HostKeySubFlowSpec.cs index 2cb5144ac..773db8c30 100644 --- a/src/TurboHTTP.StreamTests/Streams/HostKeySubFlowSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/HostKeySubFlowSpec.cs @@ -1,6 +1,6 @@ using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Internal; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.StreamTests/Streams/Internal/NetworkBufferBatchStageSpec.cs b/src/TurboHTTP.StreamTests/Streams/Internal/NetworkBufferBatchStageSpec.cs index 0e7233aaf..358b4b91c 100644 --- a/src/TurboHTTP.StreamTests/Streams/Internal/NetworkBufferBatchStageSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/Internal/NetworkBufferBatchStageSpec.cs @@ -1,6 +1,6 @@ using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Internal; using TurboHTTP.Tests.Shared; @@ -21,7 +21,7 @@ private sealed class ControlItem : IOutputItem public string Name { get; } public RequestEndpoint Key { get; } = new() - { Host = "test", Port = 80, Scheme = "http", Version = new Version(1, 1) }; + { Host = "test", Port = 80, Scheme = "http", Version = new Version(1, 1) }; public ControlItem(string name = "Control") { diff --git a/src/TurboHTTP.StreamTests/Streams/Lifecycle/ClientStreamOwnerSpec.cs b/src/TurboHTTP.StreamTests/Streams/Lifecycle/ClientStreamOwnerSpec.cs index 068c63d7e..c749d28a9 100644 --- a/src/TurboHTTP.StreamTests/Streams/Lifecycle/ClientStreamOwnerSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/Lifecycle/ClientStreamOwnerSpec.cs @@ -1,6 +1,5 @@ using System.Threading.Channels; using Akka.Actor; -using Akka.TestKit.Xunit; using TurboHTTP.Streams; using TurboHTTP.Streams.Lifecycle; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.StreamTests/Streams/StageOrderingIntegrationSpec.cs b/src/TurboHTTP.StreamTests/Streams/StageOrderingIntegrationSpec.cs index f281fdcf7..2293fffa6 100644 --- a/src/TurboHTTP.StreamTests/Streams/StageOrderingIntegrationSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/StageOrderingIntegrationSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Cookies; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams; diff --git a/src/TurboHTTP.StreamTests/Streams/StageOrderingSpec.cs b/src/TurboHTTP.StreamTests/Streams/StageOrderingSpec.cs index ae70dc9e4..f35540cf9 100644 --- a/src/TurboHTTP.StreamTests/Streams/StageOrderingSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/StageOrderingSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Caching; using TurboHTTP.Protocol.Cookies; using TurboHTTP.Protocol.Semantics; diff --git a/src/TurboHTTP.StreamTests/Streams/TransportRegistrySpec.cs b/src/TurboHTTP.StreamTests/Streams/TransportRegistrySpec.cs index 43d361eb3..30fb0fdbe 100644 --- a/src/TurboHTTP.StreamTests/Streams/TransportRegistrySpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/TransportRegistrySpec.cs @@ -1,7 +1,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; namespace TurboHTTP.StreamTests.Streams; diff --git a/src/TurboHTTP.StreamTests/Streams/VersionDispatchCachingSpec.cs b/src/TurboHTTP.StreamTests/Streams/VersionDispatchCachingSpec.cs index 2238e8767..b0d571f93 100644 --- a/src/TurboHTTP.StreamTests/Streams/VersionDispatchCachingSpec.cs +++ b/src/TurboHTTP.StreamTests/Streams/VersionDispatchCachingSpec.cs @@ -2,7 +2,7 @@ using System.Net; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Internal; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/AcceptanceTestBase.cs b/src/TurboHTTP.Tests.Shared/AcceptanceTestBase.cs index 5412d8a69..11f9ed0e1 100644 --- a/src/TurboHTTP.Tests.Shared/AcceptanceTestBase.cs +++ b/src/TurboHTTP.Tests.Shared/AcceptanceTestBase.cs @@ -1,7 +1,7 @@ using System.Text; using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using Xunit; @@ -11,30 +11,30 @@ public abstract class AcceptanceTestBase : EngineTestBase { internal static IHttpProtocolEngine CreateHttp10Engine(Action? configure = null) { - var options = new Http1Options(); - configure?.Invoke(options); - return new Http10Engine(options.ToEngineOptions()); + var clientOptions = new TurboClientOptions(); + configure?.Invoke(clientOptions.Http1); + return new Http10Engine(clientOptions); } internal static IHttpProtocolEngine CreateHttp11Engine(Action? configure = null) { - var options = new Http1Options(); - configure?.Invoke(options); - return new Http11Engine(options.ToEngineOptions()); + var clientOptions = new TurboClientOptions(); + configure?.Invoke(clientOptions.Http1); + return new Http11Engine(clientOptions); } internal static IHttpProtocolEngine CreateHttp20Engine(Action? configure = null) { - var options = new Http2Options(); - configure?.Invoke(options); - return new Http20Engine(options.ToEngineOptions()); + var clientOptions = new TurboClientOptions(); + configure?.Invoke(clientOptions.Http2); + return new Http20Engine(clientOptions); } internal static IHttpProtocolEngine CreateHttp30Engine(Action? configure = null) { - var options = new Http3Options(); - configure?.Invoke(options); - return new Http30Engine(options.ToEngineOptions()); + var clientOptions = new TurboClientOptions(); + configure?.Invoke(clientOptions.Http3); + return new Http30Engine(clientOptions); } internal async Task SendScriptedAsync( diff --git a/src/TurboHTTP.Tests.Shared/EngineFakeConnectionStage.cs b/src/TurboHTTP.Tests.Shared/EngineFakeConnectionStage.cs index ef9cbdb3d..94ccb3dc1 100644 --- a/src/TurboHTTP.Tests.Shared/EngineFakeConnectionStage.cs +++ b/src/TurboHTTP.Tests.Shared/EngineFakeConnectionStage.cs @@ -1,7 +1,7 @@ using System.Threading.Channels; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/EngineTestBase.cs b/src/TurboHTTP.Tests.Shared/EngineTestBase.cs index 75210ba38..4040976ef 100644 --- a/src/TurboHTTP.Tests.Shared/EngineTestBase.cs +++ b/src/TurboHTTP.Tests.Shared/EngineTestBase.cs @@ -3,7 +3,7 @@ using Akka.Actor; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Protocol.Http3; using Xunit; @@ -167,10 +167,10 @@ static EngineTestBase() var bytes = chunk.Buffer.Span.ToArray(); switch (chunk.StreamType) { - case Http3StreamType.Control: + case (long)StreamType.Control: controlBytes.AddRange(bytes); break; - case Http3StreamType.QpackEncoder: + case (long)StreamType.QpackEncoder: // QPACK encoder instructions — not HTTP/3 frames, skip. break; default: @@ -228,4 +228,4 @@ private static async Task> DrainOutboundH2Async(H2EngineFakeConnectio return outboundBytes; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests.Shared/FakeOps.cs b/src/TurboHTTP.Tests.Shared/FakeOps.cs index 5655cc18d..d0f216217 100644 --- a/src/TurboHTTP.Tests.Shared/FakeOps.cs +++ b/src/TurboHTTP.Tests.Shared/FakeOps.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/FakeProxyStage.cs b/src/TurboHTTP.Tests.Shared/FakeProxyStage.cs index 96d55ebd7..9af87fa6b 100644 --- a/src/TurboHTTP.Tests.Shared/FakeProxyStage.cs +++ b/src/TurboHTTP.Tests.Shared/FakeProxyStage.cs @@ -2,7 +2,7 @@ using System.Threading.Channels; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/H2EngineFakeConnectionStage.cs b/src/TurboHTTP.Tests.Shared/H2EngineFakeConnectionStage.cs index 0ffe50275..0f7f33eb0 100644 --- a/src/TurboHTTP.Tests.Shared/H2EngineFakeConnectionStage.cs +++ b/src/TurboHTTP.Tests.Shared/H2EngineFakeConnectionStage.cs @@ -1,7 +1,7 @@ using System.Threading.Channels; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/H3EngineFakeConnectionStage.cs b/src/TurboHTTP.Tests.Shared/H3EngineFakeConnectionStage.cs index f33e6248b..f542cbf55 100644 --- a/src/TurboHTTP.Tests.Shared/H3EngineFakeConnectionStage.cs +++ b/src/TurboHTTP.Tests.Shared/H3EngineFakeConnectionStage.cs @@ -1,7 +1,8 @@ using System.Threading.Channels; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; +using TurboHTTP.Protocol.Http3; namespace TurboHTTP.Tests.Shared; @@ -13,8 +14,8 @@ internal sealed class H3EngineFakeConnectionStage : GraphStage _serverFrames; - public Channel<(NetworkBuffer Buffer, Http3StreamType? StreamType)> OutboundChannel { get; } = - Channel.CreateUnbounded<(NetworkBuffer, Http3StreamType?)>(); + public Channel<(NetworkBuffer Buffer, long? StreamType)> OutboundChannel { get; } = + Channel.CreateUnbounded<(NetworkBuffer, long?)>(); public Inlet In { get; } = new("h3-engine-fake.in"); public Outlet Out { get; } = new("h3-engine-fake.out"); @@ -45,11 +46,11 @@ public Logic(H3EngineFakeConnectionStage stage) : base(stage.Shape) { var item = Grab(stage.In); - // Extract stream type from Http3NetworkBuffer (control preface, QPACK encoder, etc.) - Http3StreamType? streamType = null; - if (item is Http3NetworkBuffer h3Buf) + // Extract stream type from RoutedNetworkBuffer (control preface, QPACK encoder, etc.) + long? streamType = null; + if (item is RoutedNetworkBuffer h3Buf) { - streamType = h3Buf.StreamType != Http3StreamType.None ? h3Buf.StreamType : null; + streamType = h3Buf.StreamTypeValue; } if (item is NetworkBuffer dataChunk) @@ -57,10 +58,10 @@ public Logic(H3EngineFakeConnectionStage stage) : base(stage.Shape) stage.OutboundChannel.Writer.TryWrite(( NetworkBufferTestExtensions.FromArray(dataChunk.Span.ToArray()), streamType)); dataChunk.Dispose(); - } - // Every outbound push (tagged or not) unlocks a server frame. - Unlock(); + // Only actual data unlocks a server frame — control metadata items do not. + Unlock(); + } if (!IsClosed(stage.In)) { @@ -117,18 +118,17 @@ private void PushNextFrame() var buf = NetworkBufferTestExtensions.FromArray(frameBytes); // First frame is the control stream (SETTINGS), remaining are request stream data. - var h3Buf = Http3NetworkBuffer.Rent(buf.Length); + var h3Buf = RoutedNetworkBuffer.Rent(buf.Length); buf.Span.CopyTo(h3Buf.FullMemory.Span); h3Buf.Length = buf.Length; buf.Dispose(); if (_serverFrameIndex == 1) { - h3Buf.StreamType = Http3StreamType.Control; + h3Buf.StreamTypeValue = (long)StreamType.Control; } else { - h3Buf.StreamType = Http3StreamType.Request; h3Buf.StreamId = 0; } diff --git a/src/TurboHTTP.Tests.Shared/MockTransportOperations.cs b/src/TurboHTTP.Tests.Shared/MockTransportOperations.cs index 3c2d97d1b..95becebc2 100644 --- a/src/TurboHTTP.Tests.Shared/MockTransportOperations.cs +++ b/src/TurboHTTP.Tests.Shared/MockTransportOperations.cs @@ -1,6 +1,6 @@ using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/NetworkBufferTestExtensions.cs b/src/TurboHTTP.Tests.Shared/NetworkBufferTestExtensions.cs index 238c3c253..4a5102905 100644 --- a/src/TurboHTTP.Tests.Shared/NetworkBufferTestExtensions.cs +++ b/src/TurboHTTP.Tests.Shared/NetworkBufferTestExtensions.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests.Shared/ResponseMap.cs b/src/TurboHTTP.Tests.Shared/ResponseMap.cs index 1c89391e2..2a23e060d 100644 --- a/src/TurboHTTP.Tests.Shared/ResponseMap.cs +++ b/src/TurboHTTP.Tests.Shared/ResponseMap.cs @@ -26,7 +26,8 @@ public ResponseMap On(string path, HttpStatusCode status, string body) Content = new StringContent(body) }; return response; - })); + } + )); return this; } diff --git a/src/TurboHTTP.Tests.Shared/ScriptedFakeConnectionStage.cs b/src/TurboHTTP.Tests.Shared/ScriptedFakeConnectionStage.cs index 0b1ede7cd..6a3d99bcf 100644 --- a/src/TurboHTTP.Tests.Shared/ScriptedFakeConnectionStage.cs +++ b/src/TurboHTTP.Tests.Shared/ScriptedFakeConnectionStage.cs @@ -1,7 +1,7 @@ using System.Threading.Channels; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.Tests/Caching/CacheQualifiedDirectiveSpec.cs b/src/TurboHTTP.Tests/Caching/CacheQualifiedDirectiveSpec.cs index 4aefb3d77..d078565a7 100644 --- a/src/TurboHTTP.Tests/Caching/CacheQualifiedDirectiveSpec.cs +++ b/src/TurboHTTP.Tests/Caching/CacheQualifiedDirectiveSpec.cs @@ -17,7 +17,7 @@ private static HttpResponseMessage OkResponseWithCacheControl(string cacheContro r.Headers.Date = _baseTime; return r; } - + private static void Put(Cache store, HttpRequestMessage request, HttpResponseMessage response, byte[] body, DateTimeOffset requestTime, DateTimeOffset responseTime) { diff --git a/src/TurboHTTP.Tests/Diagnostics/LoggerTraceListenerSpec.cs b/src/TurboHTTP.Tests/Diagnostics/LoggerTraceListenerSpec.cs index 5788b7980..0a5f1ac69 100644 --- a/src/TurboHTTP.Tests/Diagnostics/LoggerTraceListenerSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/LoggerTraceListenerSpec.cs @@ -20,7 +20,7 @@ public void Constructor_should_create_logger_per_category() { _ = new LoggerTraceListener(_factory); - Assert.Equal(10, _factory.CreatedLoggers.Count); + Assert.Equal(5, _factory.CreatedLoggers.Count); } [Fact(Timeout = 5000)] @@ -126,7 +126,7 @@ public void IsEnabled_should_respect_category_filter() var listener = new LoggerTraceListener(_factory, TurboTraceCategory.Protocol); Assert.True(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Protocol)); - Assert.False(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Connection)); + Assert.False(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Request)); } [Fact(Timeout = 5000)] @@ -173,16 +173,11 @@ public void LoggerNames_should_follow_pattern() var expectedNames = new[] { - "TurboHTTP.Trace.Connection", "TurboHTTP.Trace.Protocol", "TurboHTTP.Trace.Request", - "TurboHTTP.Trace.Response", "TurboHTTP.Trace.Cache", "TurboHTTP.Trace.Redirect", - "TurboHTTP.Trace.Retry", - "TurboHTTP.Trace.Pool", - "TurboHTTP.Trace.Transport", - "TurboHTTP.Trace.Stream", + "TurboHTTP.Trace.Retry" }; foreach (var name in expectedNames) @@ -196,10 +191,10 @@ public void CombinedCategoryFilter_should_work() { var listener = new LoggerTraceListener( _factory, - TurboTraceCategory.Protocol | TurboTraceCategory.Connection); + TurboTraceCategory.Protocol | TurboTraceCategory.Redirect); Assert.True(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Protocol)); - Assert.True(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Connection)); + Assert.True(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Redirect)); Assert.False(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Request)); Assert.False(listener.IsEnabled(TurboTraceLevel.Debug, TurboTraceCategory.Cache)); } @@ -256,4 +251,4 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } private sealed record LogEntry(LogLevel Level, string Message); -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Diagnostics/ServusInstrumentationSpec.cs b/src/TurboHTTP.Tests/Diagnostics/ServusInstrumentationSpec.cs new file mode 100644 index 000000000..e1c27d7c1 --- /dev/null +++ b/src/TurboHTTP.Tests/Diagnostics/ServusInstrumentationSpec.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; +using Servus.Akka.Diagnostics; + +namespace TurboHTTP.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class ServusInstrumentationSpec : IDisposable +{ + private readonly List _activities = []; + private readonly ActivityListener _listener; + + public ServusInstrumentationSpec() + { + _listener = new ActivityListener + { + ShouldListenTo = source => source.Name == ServusInstrumentation.SourceName, + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => _activities.Add(activity) + }; + ActivitySource.AddActivityListener(_listener); + } + + public void Dispose() + { + _listener.Dispose(); + foreach (var activity in _activities) + { + if (!activity.IsStopped) + { + activity.Stop(); + } + } + } + + [Fact(Timeout = 5000)] + public void Source_should_have_correct_name() + { + Assert.Equal("Servus.Akka", ServusInstrumentation.Source.Name); + Assert.Equal("Servus.Akka", ServusInstrumentation.SourceName); + } + + [Fact(Timeout = 5000)] + public void StartConnect_should_create_activity() + { + var activity = ServusInstrumentation.StartConnect(new Uri("https://example.com:8443/")); + + Assert.NotNull(activity); + Assert.Equal("Servus.Akka.Connect", activity.OperationName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("example.com", activity.GetTagItem("server.address")); + Assert.Equal(8443, activity.GetTagItem("server.port")); + Assert.Equal("https", activity.GetTagItem("url.scheme")); + } + + [Fact(Timeout = 5000)] + public void StartDnsLookup_should_create_activity() + { + var activity = ServusInstrumentation.StartDnsLookup("example.com"); + + Assert.NotNull(activity); + Assert.Equal("Servus.Akka.DnsLookup", activity.OperationName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("example.com", activity.GetTagItem("dns.question.name")); + } + + [Fact(Timeout = 5000)] + public void StartSocketConnect_should_create_activity() + { + var activity = ServusInstrumentation.StartSocketConnect("93.184.216.34", 443); + + Assert.NotNull(activity); + Assert.Equal("Servus.Akka.SocketConnect", activity.OperationName); + Assert.Equal("93.184.216.34", activity.GetTagItem("network.peer.address")); + Assert.Equal(443, activity.GetTagItem("network.peer.port")); + Assert.Equal("tcp", activity.GetTagItem("network.transport")); + } + + [Fact(Timeout = 5000)] + public void StartSocketConnect_should_set_network_type_when_provided() + { + var activity = ServusInstrumentation.StartSocketConnect("93.184.216.34", 443, "tcp", "ipv4"); + + Assert.NotNull(activity); + Assert.Equal("ipv4", activity.GetTagItem("network.type")); + } + + [Fact(Timeout = 5000)] + public void StartSocketConnect_should_omit_network_type_when_null() + { + var activity = ServusInstrumentation.StartSocketConnect("93.184.216.34", 443); + + Assert.NotNull(activity); + Assert.Null(activity.GetTagItem("network.type")); + } + + [Fact(Timeout = 5000)] + public void StartTlsHandshake_should_create_activity() + { + var activity = ServusInstrumentation.StartTlsHandshake("example.com"); + + Assert.NotNull(activity); + Assert.Equal("Servus.Akka.TlsHandshake", activity.OperationName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("example.com", activity.GetTagItem("server.address")); + } + + [Fact(Timeout = 5000)] + public void StartWaitForConnection_should_create_activity() + { + var activity = ServusInstrumentation.StartWaitForConnection("example.com", 443); + + Assert.NotNull(activity); + Assert.Equal("Servus.Akka.WaitForConnection", activity.OperationName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("example.com", activity.GetTagItem("server.address")); + Assert.Equal(443, activity.GetTagItem("server.port")); + } + + [Fact(Timeout = 5000)] + public void SetTlsInfo_should_set_protocol_tags() + { + var activity = ServusInstrumentation.StartTlsHandshake("example.com"); + Assert.NotNull(activity); + + ServusInstrumentation.SetTlsInfo(activity, "tls", "1.3"); + + Assert.Equal("tls", activity.GetTagItem("tls.protocol.name")); + Assert.Equal("1.3", activity.GetTagItem("tls.protocol.version")); + } + + [Fact(Timeout = 5000)] + public void SetDnsAnswers_should_set_answers_tag() + { + var activity = ServusInstrumentation.StartDnsLookup("example.com"); + Assert.NotNull(activity); + + ServusInstrumentation.SetDnsAnswers(activity, ["93.184.216.34", "2606:2800:220:1::"]); + + Assert.Equal(new[] { "93.184.216.34", "2606:2800:220:1::" }, activity.GetTagItem("dns.answers")); + } + + [Fact(Timeout = 5000)] + public void SetNetworkPeerAddress_should_set_tag() + { + var activity = ServusInstrumentation.StartConnect(new Uri("https://example.com/")); + Assert.NotNull(activity); + + ServusInstrumentation.SetNetworkPeerAddress(activity, "93.184.216.34"); + + Assert.Equal("93.184.216.34", activity.GetTagItem("network.peer.address")); + } + + [Fact(Timeout = 5000)] + public void SetError_should_set_error_status_and_type() + { + var activity = ServusInstrumentation.StartConnect(new Uri("https://example.com/")); + Assert.NotNull(activity); + + var ex = new InvalidOperationException("test error"); + ServusInstrumentation.SetError(activity, ex); + + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal(typeof(InvalidOperationException).FullName, activity.GetTagItem("error.type")); + } + + [Fact(Timeout = 5000)] + public void StartConnect_should_return_null_when_no_listener() + { + _listener.Dispose(); + var activity = ServusInstrumentation.StartConnect(new Uri("https://example.com/")); + Assert.Null(activity); + } +} diff --git a/src/TurboHTTP.Tests/Diagnostics/ServusMetricsSpec.cs b/src/TurboHTTP.Tests/Diagnostics/ServusMetricsSpec.cs new file mode 100644 index 000000000..6c784fcb4 --- /dev/null +++ b/src/TurboHTTP.Tests/Diagnostics/ServusMetricsSpec.cs @@ -0,0 +1,207 @@ +using System.Collections.Concurrent; +using System.Diagnostics.Metrics; +using Servus.Akka.Diagnostics; + +namespace TurboHTTP.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class ServusMetricsSpec : IDisposable +{ + private readonly MeterListener _listener; + private readonly ConcurrentBag> _longMeasurements = []; + private readonly ConcurrentBag> _doubleMeasurements = []; + + public ServusMetricsSpec() + { + _listener = new MeterListener(); + _listener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == ServusMetrics.MeterName) + { + listener.EnableMeasurementEvents(instrument); + } + }; + + _listener.SetMeasurementEventCallback( + (instrument, measurement, tags, _) => + _longMeasurements.Add(new MetricMeasurement(instrument.Name, measurement, tags))); + + _listener.SetMeasurementEventCallback( + (instrument, measurement, tags, _) => + _doubleMeasurements.Add(new MetricMeasurement(instrument.Name, measurement, tags))); + + _listener.Start(); + } + + public void Dispose() + { + _listener.Dispose(); + } + + [Fact(Timeout = 5000)] + public void Meter_should_have_correct_name() + { + Assert.Equal("Servus.Akka", ServusMetrics.Meter.Name); + Assert.Equal("Servus.Akka", ServusMetrics.MeterName); + } + + [Fact(Timeout = 5000)] + public void OpenConnections_should_increment_active() + { + ClearMeasurements(); + + ServusMetrics.OpenConnections.Add(1, + new KeyValuePair("http.connection.state", "active"), + new KeyValuePair("server.address", "pool.example.com"), + new KeyValuePair("server.port", 443)); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetLongMeasurements("http.client.open_connections")); + Assert.Equal(1, m.Value); + Assert.Equal("active", GetTag(m.Tags, "http.connection.state")); + } + + [Fact(Timeout = 5000)] + public void OpenConnections_should_decrement_active() + { + ClearMeasurements(); + + ServusMetrics.OpenConnections.Add(1, + new KeyValuePair("http.connection.state", "active"), + new KeyValuePair("server.address", "pool.example.com"), + new KeyValuePair("server.port", 443)); + ServusMetrics.OpenConnections.Add(-1, + new KeyValuePair("http.connection.state", "active"), + new KeyValuePair("server.address", "pool.example.com"), + new KeyValuePair("server.port", 443)); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("http.client.open_connections"); + Assert.Equal(2, measurements.Count); + Assert.Contains(measurements, m => m.Value == 1); + Assert.Contains(measurements, m => m.Value == -1); + } + + [Fact(Timeout = 5000)] + public void OpenConnections_should_distinguish_active_and_idle() + { + ClearMeasurements(); + + ServusMetrics.OpenConnections.Add(1, + new KeyValuePair("http.connection.state", "active")); + ServusMetrics.OpenConnections.Add(1, + new KeyValuePair("http.connection.state", "idle")); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("http.client.open_connections"); + Assert.Equal(2, measurements.Count); + Assert.Contains(measurements, m => GetTag(m.Tags, "http.connection.state")?.ToString() == "active"); + Assert.Contains(measurements, m => GetTag(m.Tags, "http.connection.state")?.ToString() == "idle"); + } + + [Fact(Timeout = 5000)] + public void ConnectionDuration_should_record() + { + ClearMeasurements(); + + ServusMetrics.ConnectionDuration.Record(30.5, + new KeyValuePair("server.address", "conn.example.com"), + new KeyValuePair("server.port", 443)); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("http.client.connection.duration")); + Assert.Equal(30.5, m.Value); + Assert.Equal("conn.example.com", GetTag(m.Tags, "server.address")); + } + + [Fact(Timeout = 5000)] + public void RequestTimeInQueue_should_record() + { + ClearMeasurements(); + + ServusMetrics.RequestTimeInQueue.Record(0.050, + new KeyValuePair("server.address", "example.com"), + new KeyValuePair("server.port", 443)); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("http.client.request.time_in_queue")); + Assert.Equal(0.050, m.Value); + } + + [Fact(Timeout = 5000)] + public void DnsLookupDuration_should_record() + { + ClearMeasurements(); + + ServusMetrics.DnsLookupDuration.Record(0.015, + new KeyValuePair("dns.question.name", "example.com")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("dns.lookup.duration")); + Assert.Equal(0.015, m.Value); + Assert.Equal("example.com", GetTag(m.Tags, "dns.question.name")); + } + + [Fact(Timeout = 5000)] + public void Instruments_should_have_correct_units() + { + Assert.Equal("{connection}", ServusMetrics.OpenConnections.Unit); + Assert.Equal("s", ServusMetrics.ConnectionDuration.Unit); + Assert.Equal("s", ServusMetrics.RequestTimeInQueue.Unit); + Assert.Equal("s", ServusMetrics.DnsLookupDuration.Unit); + } + + [Fact(Timeout = 5000)] + public void Instruments_should_have_descriptions() + { + Assert.False(string.IsNullOrEmpty(ServusMetrics.OpenConnections.Description)); + Assert.False(string.IsNullOrEmpty(ServusMetrics.ConnectionDuration.Description)); + Assert.False(string.IsNullOrEmpty(ServusMetrics.RequestTimeInQueue.Description)); + Assert.False(string.IsNullOrEmpty(ServusMetrics.DnsLookupDuration.Description)); + } + + private void ClearMeasurements() + { + _longMeasurements.Clear(); + _doubleMeasurements.Clear(); + } + + private List> GetLongMeasurements(string name) => + _longMeasurements.Where(m => m.InstrumentName == name).ToList(); + + private List> GetDoubleMeasurements(string name) => + _doubleMeasurements.Where(m => m.InstrumentName == name).ToList(); + + private static object? GetTag(ReadOnlySpan> tags, string key) + { + foreach (var tag in tags) + { + if (tag.Key == key) + { + return tag.Value; + } + } + + return null; + } + + private readonly record struct MetricMeasurement where T : struct + { + public string InstrumentName { get; } + public T Value { get; } + public KeyValuePair[] Tags { get; } + + public MetricMeasurement(string instrumentName, T value, ReadOnlySpan> tags) + { + InstrumentName = instrumentName; + Value = value; + Tags = tags.ToArray(); + } + } +} diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboHttpDiagnosticSourceSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboHttpDiagnosticSourceSpec.cs deleted file mode 100644 index 9e94ec814..000000000 --- a/src/TurboHTTP.Tests/Diagnostics/TurboHttpDiagnosticSourceSpec.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Diagnostics; -using TurboHTTP.Diagnostics; - -namespace TurboHTTP.Tests.Diagnostics; - -public sealed class TurboHttpDiagnosticSourceSpec : IDisposable -{ - private readonly List> _events = []; - private readonly IDisposable _subscription; - - public TurboHttpDiagnosticSourceSpec() - { - var observer = new TestObserver(_events); - _subscription = DiagnosticListener.AllListeners.Subscribe(new TestListenerObserver(observer)); - } - - public void Dispose() - { - _subscription.Dispose(); - } - - [Fact(Timeout = 5000)] - public void ListenerName_should_be_TurboHTTP() - { - Assert.Equal("TurboHTTP", TurboHttpDiagnosticSource.ListenerName); - } - - [Fact(Timeout = 5000)] - public void OnRequestStart_should_emit_event() - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/"); - - TurboHttpDiagnosticSource.OnRequestStart(request); - - var evt = _events.FirstOrDefault(e => e.Key == "TurboHTTP.HttpRequestOut.Start"); - Assert.NotNull(evt.Value); - } - - [Fact(Timeout = 5000)] - public void OnRequestStop_should_emit_event() - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/"); - var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - - TurboHttpDiagnosticSource.OnRequestStop(request, response, TaskStatus.RanToCompletion); - - var evt = _events.FirstOrDefault(e => e.Key == "TurboHTTP.HttpRequestOut.Stop"); - Assert.NotNull(evt.Value); - } - - [Fact(Timeout = 5000)] - public void OnException_should_emit_event() - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/"); - var exception = new HttpRequestException("Connection refused"); - - TurboHttpDiagnosticSource.OnException(request, exception); - - var evt = _events.FirstOrDefault(e => e.Key == "TurboHTTP.Exception"); - Assert.NotNull(evt.Value); - } - - private sealed class TestListenerObserver(TestObserver inner) : IObserver - { - public void OnNext(DiagnosticListener value) - { - if (value.Name == TurboHttpDiagnosticSource.ListenerName) - { - value.Subscribe(inner); - } - } - - public void OnError(Exception error) { } - public void OnCompleted() { } - } - - private sealed class TestObserver(List> events) - : IObserver> - { - public void OnNext(KeyValuePair value) => events.Add(value); - public void OnError(Exception error) { } - public void OnCompleted() { } - } -} diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboHttpEventSourceSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboHttpEventSourceSpec.cs deleted file mode 100644 index 9d7b0f9ed..000000000 --- a/src/TurboHTTP.Tests/Diagnostics/TurboHttpEventSourceSpec.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Diagnostics.Tracing; -using TurboHTTP.Diagnostics; - -namespace TurboHTTP.Tests.Diagnostics; - -[Collection("OTEL")] -public sealed class TurboHttpEventSourceSpec : IDisposable -{ - private readonly TestEventListener _listener; - - public TurboHttpEventSourceSpec() - { - _listener = new TestEventListener(); - _listener.EnableEvents(TurboHttpEventSource.Instance, EventLevel.Verbose, EventKeywords.All); - } - - public void Dispose() - { - _listener.Dispose(); - } - - [Fact(Timeout = 5000)] - public void EventSource_should_have_correct_name() - { - Assert.Equal("TurboHTTP", TurboHttpEventSource.Instance.Name); - } - - [Fact(Timeout = 5000)] - public void RequestStart_should_emit_event() - { - TurboHttpEventSource.Instance.RequestStart("GET", "https://example.com/"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 1); - Assert.NotNull(evt); - Assert.Equal("GET", evt.Payload?[0]); - } - - [Fact(Timeout = 5000)] - public void RequestStop_should_emit_event() - { - TurboHttpEventSource.Instance.RequestStop("GET", 200, 42.5); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 2); - Assert.NotNull(evt); - Assert.Equal(200, evt.Payload?[1]); - } - - [Fact(Timeout = 5000)] - public void RequestFailed_should_emit_event() - { - TurboHttpEventSource.Instance.RequestFailed("GET", "https://example.com/", "HttpRequestException"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 3); - Assert.NotNull(evt); - Assert.Equal("HttpRequestException", evt.Payload?[2]); - } - - [Fact(Timeout = 5000)] - public void ConnectionStart_should_emit_event() - { - TurboHttpEventSource.Instance.ConnectionStart("example.com", 443); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 10); - Assert.NotNull(evt); - Assert.Equal("example.com", evt.Payload?[0]); - } - - [Fact(Timeout = 5000)] - public void ConnectionStop_should_emit_event() - { - TurboHttpEventSource.Instance.ConnectionStop("example.com", 443, 1234.5); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 11); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void DnsLookupStart_should_emit_event() - { - TurboHttpEventSource.Instance.DnsLookupStart("example.com"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 20); - Assert.NotNull(evt); - Assert.Equal("example.com", evt.Payload?[0]); - } - - [Fact(Timeout = 5000)] - public void DnsLookupStop_should_emit_event() - { - TurboHttpEventSource.Instance.DnsLookupStop("example.com", 5.2); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 21); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void TlsHandshakeStart_should_emit_event() - { - TurboHttpEventSource.Instance.TlsHandshakeStart("example.com"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 30); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void TlsHandshakeStop_should_emit_event() - { - TurboHttpEventSource.Instance.TlsHandshakeStop("example.com", 15.3); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 31); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void Redirect_should_emit_event() - { - TurboHttpEventSource.Instance.Redirect(301, "https://example.com/new"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 40); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void RetryAttempt_should_emit_event() - { - TurboHttpEventSource.Instance.RetryAttempt(2); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 50); - Assert.NotNull(evt); - Assert.Equal(2, evt.Payload?[0]); - } - - [Fact(Timeout = 5000)] - public void CacheHit_should_emit_event() - { - TurboHttpEventSource.Instance.CacheHit("https://example.com/cached"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 60); - Assert.NotNull(evt); - } - - [Fact(Timeout = 5000)] - public void CacheMiss_should_emit_event() - { - TurboHttpEventSource.Instance.CacheMiss("https://example.com/uncached"); - - var evt = _listener.Events.FirstOrDefault(e => e.EventId == 61); - Assert.NotNull(evt); - } - - private sealed class TestEventListener : EventListener - { - public List Events { get; } = []; - - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - Events.Add(eventData); - } - } -} diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboHttpInstrumentationSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboHttpInstrumentationSpec.cs index 97bcaaab0..7bb13e3e2 100644 --- a/src/TurboHTTP.Tests/Diagnostics/TurboHttpInstrumentationSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/TurboHttpInstrumentationSpec.cs @@ -699,121 +699,6 @@ public void SetError_should_set_error_type_tag() Assert.Equal(typeof(HttpRequestException).FullName, activity.GetTagItem("error.type")); } - [Fact(Timeout = 5000)] - public void StartDnsLookup_should_create_activity() - { - var activity = TurboHttpInstrumentation.StartDnsLookup("example.com"); - - Assert.NotNull(activity); - Assert.Equal("TurboHTTP.DnsLookup", activity.OperationName); - Assert.Equal("example.com", activity.GetTagItem("dns.question.name")); - } - - [Fact(Timeout = 5000)] - public void StartSocketConnect_should_create_activity() - { - var activity = TurboHttpInstrumentation.StartSocketConnect("93.184.216.34", 443); - - Assert.NotNull(activity); - Assert.Equal("TurboHTTP.SocketConnect", activity.OperationName); - Assert.Equal("93.184.216.34", activity.GetTagItem("network.peer.address")); - Assert.Equal(443, activity.GetTagItem("network.peer.port")); - Assert.Equal("tcp", activity.GetTagItem("network.transport")); - } - - [Fact(Timeout = 5000)] - public void StartSocketConnect_should_set_network_type_when_provided() - { - var activity = TurboHttpInstrumentation.StartSocketConnect("93.184.216.34", 443, "tcp", "ipv4"); - - Assert.NotNull(activity); - Assert.Equal("ipv4", activity.GetTagItem("network.type")); - } - - [Fact(Timeout = 5000)] - public void StartSocketConnect_should_omit_network_type_when_null() - { - var activity = TurboHttpInstrumentation.StartSocketConnect("93.184.216.34", 443); - - Assert.NotNull(activity); - Assert.Null(activity.GetTagItem("network.type")); - } - - [Fact(Timeout = 5000)] - public void StartTlsHandshake_should_create_activity() - { - var activity = TurboHttpInstrumentation.StartTlsHandshake("example.com"); - - Assert.NotNull(activity); - Assert.Equal("TurboHTTP.TlsHandshake", activity.OperationName); - Assert.Equal("example.com", activity.GetTagItem("server.address")); - } - - [Fact(Timeout = 5000)] - public void StartWaitForConnection_should_create_activity() - { - var activity = TurboHttpInstrumentation.StartWaitForConnection("example.com", 443); - - Assert.NotNull(activity); - Assert.Equal("TurboHTTP.WaitForConnection", activity.OperationName); - Assert.Equal("example.com", activity.GetTagItem("server.address")); - Assert.Equal(443, activity.GetTagItem("server.port")); - } - - [Fact(Timeout = 5000)] - public void StartConnect_should_create_activity() - { - var activity = TurboHttpInstrumentation.StartConnect(new Uri("https://example.com:8443/")); - - Assert.NotNull(activity); - Assert.Equal("TurboHTTP.Connect", activity.OperationName); - Assert.Equal("example.com", activity.GetTagItem("server.address")); - Assert.Equal(8443, activity.GetTagItem("server.port")); - } - - [Fact(Timeout = 5000)] - public void StartConnect_should_set_url_scheme() - { - var activity = TurboHttpInstrumentation.StartConnect(new Uri("https://example.com/")); - - Assert.NotNull(activity); - Assert.Equal("https", activity.GetTagItem("url.scheme")); - } - - [Fact(Timeout = 5000)] - public void SetTlsInfo_should_set_protocol_tags() - { - var activity = TurboHttpInstrumentation.StartTlsHandshake("example.com"); - Assert.NotNull(activity); - - TurboHttpInstrumentation.SetTlsInfo(activity, "tls", "1.3"); - - Assert.Equal("tls", activity.GetTagItem("tls.protocol.name")); - Assert.Equal("1.3", activity.GetTagItem("tls.protocol.version")); - } - - [Fact(Timeout = 5000)] - public void SetDnsAnswers_should_set_answers_tag() - { - var activity = TurboHttpInstrumentation.StartDnsLookup("example.com"); - Assert.NotNull(activity); - - TurboHttpInstrumentation.SetDnsAnswers(activity, ["93.184.216.34", "2606:2800:220:1::"]); - - Assert.Equal(new[] { "93.184.216.34", "2606:2800:220:1::" }, activity.GetTagItem("dns.answers")); - } - - [Fact(Timeout = 5000)] - public void SetNetworkPeerAddress_should_set_tag() - { - var activity = TurboHttpInstrumentation.StartConnect(new Uri("https://example.com/")); - Assert.NotNull(activity); - - TurboHttpInstrumentation.SetNetworkPeerAddress(activity, "93.184.216.34"); - - Assert.Equal("93.184.216.34", activity.GetTagItem("network.peer.address")); - } - [Fact(Timeout = 5000)] public void IsTracingActive_should_return_true_when_listener_present() { @@ -941,15 +826,6 @@ public void Source_should_be_disposable() Assert.Equal("TurboHTTP", TurboHttpInstrumentation.Source.Name); } - [Fact(Timeout = 5000)] - public void StartDnsLookup_should_return_null_when_no_listener() - { - _listener.Dispose(); - TurboHttpInstrumentation.StartDnsLookup("example.com"); - // May or may not be null depending on other listeners - Assert.Equal("TurboHTTP", TurboHttpInstrumentation.SourceName); - } - [Fact(Timeout = 5000)] public void SetResponse_with_http10_should_format_version_correctly() { diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboHttpMetricsSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboHttpMetricsSpec.cs index 1ca633a73..ea4e50e95 100644 --- a/src/TurboHTTP.Tests/Diagnostics/TurboHttpMetricsSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/TurboHttpMetricsSpec.cs @@ -229,81 +229,6 @@ public void RedirectCount_should_increment() Assert.Equal(301, GetTag(m.Tags, "http.response.status_code")); } - [Fact(Timeout = 5000)] - public void OpenConnections_should_increment_active() - { - ClearMeasurements(); - - TurboHttpMetrics.OpenConnections.Add(1, - new KeyValuePair("http.connection.state", "active"), - new KeyValuePair("server.address", "pool.example.com"), - new KeyValuePair("server.port", 443)); - - _listener.RecordObservableInstruments(); - - var m = Assert.Single(GetLongMeasurements("http.client.open_connections")); - Assert.Equal(1, m.Value); - Assert.Equal("active", GetTag(m.Tags, "http.connection.state")); - } - - [Fact(Timeout = 5000)] - public void OpenConnections_should_decrement_active() - { - ClearMeasurements(); - - TurboHttpMetrics.OpenConnections.Add(1, - new KeyValuePair("http.connection.state", "active"), - new KeyValuePair("server.address", "pool.example.com"), - new KeyValuePair("server.port", 443)); - TurboHttpMetrics.OpenConnections.Add(-1, - new KeyValuePair("http.connection.state", "active"), - new KeyValuePair("server.address", "pool.example.com"), - new KeyValuePair("server.port", 443)); - - _listener.RecordObservableInstruments(); - - var measurements = GetLongMeasurements("http.client.open_connections"); - Assert.Equal(2, measurements.Count); - Assert.Contains(measurements, m => m.Value == 1); - Assert.Contains(measurements, m => m.Value == -1); - } - - [Fact(Timeout = 5000)] - public void OpenConnections_should_track_idle() - { - ClearMeasurements(); - - TurboHttpMetrics.OpenConnections.Add(1, - new KeyValuePair("http.connection.state", "idle"), - new KeyValuePair("server.address", "idle.example.com"), - new KeyValuePair("server.port", 80)); - - _listener.RecordObservableInstruments(); - - var m = Assert.Single(GetLongMeasurements("http.client.open_connections")); - Assert.Equal(1, m.Value); - Assert.Equal("idle", GetTag(m.Tags, "http.connection.state")); - Assert.Equal("idle.example.com", GetTag(m.Tags, "server.address")); - } - - [Fact(Timeout = 5000)] - public void OpenConnections_should_distinguish_active_and_idle() - { - ClearMeasurements(); - - TurboHttpMetrics.OpenConnections.Add(1, - new KeyValuePair("http.connection.state", "active")); - TurboHttpMetrics.OpenConnections.Add(1, - new KeyValuePair("http.connection.state", "idle")); - - _listener.RecordObservableInstruments(); - - var measurements = GetLongMeasurements("http.client.open_connections"); - Assert.Equal(2, measurements.Count); - Assert.Contains(measurements, m => GetTag(m.Tags, "http.connection.state")?.ToString() == "active"); - Assert.Contains(measurements, m => GetTag(m.Tags, "http.connection.state")?.ToString() == "idle"); - } - [Fact(Timeout = 5000)] public void RequestDuration_should_record() { @@ -321,22 +246,6 @@ public void RequestDuration_should_record() Assert.Equal(200, GetTag(m.Tags, "http.response.status_code")); } - [Fact(Timeout = 5000)] - public void ConnectionDuration_should_record() - { - ClearMeasurements(); - - TurboHttpMetrics.ConnectionDuration.Record(30.5, - new KeyValuePair("server.address", "conn.example.com"), - new KeyValuePair("server.port", 443)); - - _listener.RecordObservableInstruments(); - - var m = Assert.Single(GetDoubleMeasurements("http.client.connection.duration")); - Assert.Equal(30.5, m.Value); - } - - [Fact(Timeout = 5000)] public void ActiveRequests_should_increment_and_decrement() { @@ -361,38 +270,6 @@ public void ActiveRequests_should_increment_and_decrement() Assert.Equal(0, measurements.Sum(m => m.Value)); } - [Fact(Timeout = 5000)] - public void RequestTimeInQueue_should_record() - { - ClearMeasurements(); - - TurboHttpMetrics.RequestTimeInQueue.Record(0.050, - new KeyValuePair("http.request.method", "GET"), - new KeyValuePair("server.address", "example.com"), - new KeyValuePair("server.port", 443), - new KeyValuePair("url.scheme", "https")); - - _listener.RecordObservableInstruments(); - - var m = Assert.Single(GetDoubleMeasurements("http.client.request.time_in_queue")); - Assert.Equal(0.050, m.Value); - } - - [Fact(Timeout = 5000)] - public void DnsLookupDuration_should_record() - { - ClearMeasurements(); - - TurboHttpMetrics.DnsLookupDuration.Record(0.015, - new KeyValuePair("dns.question.name", "example.com")); - - _listener.RecordObservableInstruments(); - - var m = Assert.Single(GetDoubleMeasurements("dns.lookup.duration")); - Assert.Equal(0.015, m.Value); - Assert.Equal("example.com", GetTag(m.Tags, "dns.question.name")); - } - [Fact(Timeout = 5000)] public void PipelineStall_should_increment() { @@ -417,11 +294,7 @@ public void Instruments_should_have_correct_units() Assert.Equal("{miss}", TurboHttpMetrics.CacheMiss.Unit); Assert.Equal("{retry}", TurboHttpMetrics.RetryCount.Unit); Assert.Equal("{redirect}", TurboHttpMetrics.RedirectCount.Unit); - Assert.Equal("s", TurboHttpMetrics.ConnectionDuration.Unit); - Assert.Equal("{connection}", TurboHttpMetrics.OpenConnections.Unit); Assert.Equal("{request}", TurboHttpMetrics.ActiveRequests.Unit); - Assert.Equal("s", TurboHttpMetrics.RequestTimeInQueue.Unit); - Assert.Equal("s", TurboHttpMetrics.DnsLookupDuration.Unit); Assert.Equal("{stall}", TurboHttpMetrics.PipelineStall.Unit); } @@ -434,11 +307,7 @@ public void Instruments_should_have_descriptions() Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.CacheMiss.Description)); Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.RetryCount.Description)); Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.RedirectCount.Description)); - Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.ConnectionDuration.Description)); - Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.OpenConnections.Description)); Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.ActiveRequests.Description)); - Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.RequestTimeInQueue.Description)); - Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.DnsLookupDuration.Description)); Assert.False(string.IsNullOrEmpty(TurboHttpMetrics.PipelineStall.Description)); } diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboTraceCategoryMethodsSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboTraceCategoryMethodsSpec.cs index af16491ea..24e6d3ae8 100644 --- a/src/TurboHTTP.Tests/Diagnostics/TurboTraceCategoryMethodsSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/TurboTraceCategoryMethodsSpec.cs @@ -19,106 +19,6 @@ public void Dispose() TurboTrace.Disable(); } - #region Connection Category Tests - - [Fact(Timeout = 5000)] - public void Connection_Trace_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Trace(this, "connection trace"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Trace, evt.Level); - Assert.Equal(TurboTraceCategory.Connection, evt.Category); - Assert.Equal("connection trace", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Connection_Trace_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Trace(this, "id={0}", 42); - var evt = Assert.Single(_mock.Events); - Assert.Equal("id=42", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Connection_Debug_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Debug(this, "debug msg"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Debug, evt.Level); - Assert.Equal(TurboTraceCategory.Connection, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Connection_Debug_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Debug(this, "{0}:{1}", "host", 443); - var evt = Assert.Single(_mock.Events); - Assert.Equal("host:443", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Connection_Info_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Info(this, "info"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Info, evt.Level); - Assert.Equal(TurboTraceCategory.Connection, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Connection_Info_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Info(this, "port={0}", 8080); - var evt = Assert.Single(_mock.Events); - Assert.Equal("port=8080", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Connection_Warning_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Warning(this, "warn"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Connection, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Connection_Warning_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Warning(this, "timeout {0}ms", 5000); - var evt = Assert.Single(_mock.Events); - Assert.Equal("timeout 5000ms", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Connection_Error_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Error(this, "error"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Error, evt.Level); - Assert.Equal(TurboTraceCategory.Connection, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Connection_Error_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Connection.Error(this, "failed: {0}", "refused"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("failed: refused", evt.FormatMessage()); - } - - #endregion - #region Protocol Category Tests [Fact(Timeout = 5000)] @@ -318,105 +218,6 @@ public void Request_Error_with_args_should_format() #endregion - #region Response Category Tests - - [Fact(Timeout = 5000)] - public void Response_Trace_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Trace(this, "resp trace"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Trace, evt.Level); - Assert.Equal(TurboTraceCategory.Response, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Response_Trace_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Trace(this, "status={0}", 200); - var evt = Assert.Single(_mock.Events); - Assert.Equal("status=200", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Response_Debug_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Debug(this, "resp debug"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Debug, evt.Level); - Assert.Equal(TurboTraceCategory.Response, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Response_Debug_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Debug(this, "size={0}", 512); - var evt = Assert.Single(_mock.Events); - Assert.Equal("size=512", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Response_Info_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Info(this, "resp info"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Info, evt.Level); - Assert.Equal(TurboTraceCategory.Response, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Response_Info_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Info(this, "code={0}", 404); - var evt = Assert.Single(_mock.Events); - Assert.Equal("code=404", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Response_Warning_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Warning(this, "resp warn"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Response, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Response_Warning_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Warning(this, "delay {0}ms", 1000); - var evt = Assert.Single(_mock.Events); - Assert.Equal("delay 1000ms", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Response_Error_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Error(this, "resp error"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Error, evt.Level); - Assert.Equal(TurboTraceCategory.Response, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Response_Error_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Response.Error(this, "error: {0}", "timeout"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("error: timeout", evt.FormatMessage()); - } - - #endregion - #region Cache Category Tests [Fact(Timeout = 5000)] @@ -714,300 +515,4 @@ public void Retry_Error_with_args_should_format() #endregion - #region Pool Category Tests - - [Fact(Timeout = 5000)] - public void Pool_Trace_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Trace(this, "pool trace"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Trace, evt.Level); - Assert.Equal(TurboTraceCategory.Pool, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Pool_Trace_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Trace(this, "size={0}", 10); - var evt = Assert.Single(_mock.Events); - Assert.Equal("size=10", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Pool_Debug_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Debug(this, "pool debug"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Debug, evt.Level); - Assert.Equal(TurboTraceCategory.Pool, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Pool_Debug_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Debug(this, "available={0}", 8); - var evt = Assert.Single(_mock.Events); - Assert.Equal("available=8", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Pool_Info_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Info(this, "pool info"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Info, evt.Level); - Assert.Equal(TurboTraceCategory.Pool, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Pool_Info_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Info(this, "size={0}", 16); - var evt = Assert.Single(_mock.Events); - Assert.Equal("size=16", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Pool_Warning_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Warning(this, "pool warn"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Pool, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Pool_Warning_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Warning(this, "leak {0}", "suspected"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("leak suspected", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Pool_Error_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Error(this, "pool error"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Error, evt.Level); - Assert.Equal(TurboTraceCategory.Pool, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Pool_Error_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Pool.Error(this, "error: {0}", "exhausted"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("error: exhausted", evt.FormatMessage()); - } - - #endregion - - #region Transport Category Tests - - [Fact(Timeout = 5000)] - public void Transport_Trace_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Trace(this, "transport trace"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Trace, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Transport_Trace_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Trace(this, "protocol={0}", "TCP"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("protocol=TCP", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Transport_Debug_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Debug(this, "transport debug"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Debug, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Transport_Debug_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Debug(this, "port={0}", 443); - var evt = Assert.Single(_mock.Events); - Assert.Equal("port=443", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Transport_Info_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Info(this, "transport info"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Info, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Transport_Info_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Info(this, "bytes={0}", 4096); - var evt = Assert.Single(_mock.Events); - Assert.Equal("bytes=4096", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Transport_Warning_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Warning(this, "transport warn"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Transport_Warning_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Warning(this, "slow {0}ms", 2000); - var evt = Assert.Single(_mock.Events); - Assert.Equal("slow 2000ms", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Transport_Error_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Error(this, "transport error"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Error, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Transport_Error_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Transport.Error(this, "error: {0}", "reset"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("error: reset", evt.FormatMessage()); - } - - #endregion - - #region Stream Category Tests - - [Fact(Timeout = 5000)] - public void Stream_Trace_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Trace(this, "stream trace"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Trace, evt.Level); - Assert.Equal(TurboTraceCategory.Stream, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Stream_Trace_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Trace(this, "id={0}", 123); - var evt = Assert.Single(_mock.Events); - Assert.Equal("id=123", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Stream_Debug_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Debug(this, "stream debug"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Debug, evt.Level); - Assert.Equal(TurboTraceCategory.Stream, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Stream_Debug_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Debug(this, "buffer={0}", 2048); - var evt = Assert.Single(_mock.Events); - Assert.Equal("buffer=2048", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Stream_Info_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Info(this, "stream info"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Info, evt.Level); - Assert.Equal(TurboTraceCategory.Stream, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Stream_Info_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Info(this, "stage={0}", "encoding"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("stage=encoding", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Stream_Warning_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Warning(this, "stream warn"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Stream, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Stream_Warning_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Warning(this, "backpressure {0}ms", 300); - var evt = Assert.Single(_mock.Events); - Assert.Equal("backpressure 300ms", evt.FormatMessage()); - } - - [Fact(Timeout = 5000)] - public void Stream_Error_should_write_event() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Error(this, "stream error"); - var evt = Assert.Single(_mock.Events); - Assert.Equal(TurboTraceLevel.Error, evt.Level); - Assert.Equal(TurboTraceCategory.Stream, evt.Category); - } - - [Fact(Timeout = 5000)] - public void Stream_Error_with_args_should_format() - { - TurboTrace.Configure(_mock); - TurboTrace.Stream.Error(this, "error: {0}", "malformed"); - var evt = Assert.Single(_mock.Events); - Assert.Equal("error: malformed", evt.FormatMessage()); - } - - #endregion } diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboTraceExtensionsSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboTraceExtensionsSpec.cs index 005db524e..919939d6e 100644 --- a/src/TurboHTTP.Tests/Diagnostics/TurboTraceExtensionsSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/TurboTraceExtensionsSpec.cs @@ -1,7 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; using TurboHTTP.Diagnostics; namespace TurboHTTP.Tests.Diagnostics; @@ -55,7 +52,7 @@ public void AddTurboLoggerTracing_should_filter_by_category() _ = provider.GetRequiredService(); Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Protocol, TurboTraceLevel.Debug)); - Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Connection, TurboTraceLevel.Debug)); + Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Redirect, TurboTraceLevel.Debug)); } [Fact(Timeout = 5000)] @@ -144,7 +141,7 @@ public void AddTurboTracing_should_filter_by_category() _ = provider.GetRequiredService(); Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Request, TurboTraceLevel.Debug)); - Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Response, TurboTraceLevel.Debug)); + Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Retry, TurboTraceLevel.Debug)); } [Fact(Timeout = 5000)] @@ -162,26 +159,6 @@ public void AddTurboTracing_should_filter_by_minimum_level() Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Protocol, TurboTraceLevel.Info)); } - [Fact(Timeout = 5000)] - public void AddTurboHttpMetrics_should_add_meter() - { - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddTurboHttpMetrics() - .Build(); - - Assert.NotNull(meterProvider); - } - - [Fact(Timeout = 5000)] - public void AddTurboHttpTracing_should_add_source() - { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddTurboHttpTracing() - .Build(); - - Assert.NotNull(tracerProvider); - } - private sealed class MockTraceListener : ITurboTraceListener { public List Events { get; } = []; @@ -190,4 +167,4 @@ private sealed class MockTraceListener : ITurboTraceListener public void Write(in TraceEvent evt) => Events.Add(evt); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboTraceSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboTraceSpec.cs index bbd98f267..5eeeb8bbb 100644 --- a/src/TurboHTTP.Tests/Diagnostics/TurboTraceSpec.cs +++ b/src/TurboHTTP.Tests/Diagnostics/TurboTraceSpec.cs @@ -78,11 +78,11 @@ public void TraceEvent_should_capture_timestamp() public void TraceEvent_should_store_level_and_category() { var evt = new TraceEvent( - 0, TurboTraceLevel.Warning, TurboTraceCategory.Transport, + 0, TurboTraceLevel.Warning, TurboTraceCategory.Cache, "Test", 0, "msg"); Assert.Equal(TurboTraceLevel.Warning, evt.Level); - Assert.Equal(TurboTraceCategory.Transport, evt.Category); + Assert.Equal(TurboTraceCategory.Cache, evt.Category); } [Fact(Timeout = 5000)] @@ -130,14 +130,6 @@ public void ShouldTrace_should_return_true_when_enabled() Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Protocol, TurboTraceLevel.Debug)); } - [Fact(Timeout = 5000)] - public void ShouldTrace_should_return_false_when_category_disabled() - { - TurboTrace.Configure(_mock, TurboTraceCategory.Connection); - - Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Protocol, TurboTraceLevel.Debug)); - } - [Fact(Timeout = 5000)] public void ShouldTrace_should_return_false_when_below_minimum() { @@ -178,17 +170,6 @@ public void ProtocolDebug_should_write_correct_category() Assert.Equal(TurboTraceCategory.Protocol, _mock.Events[0].Category); } - [Fact(Timeout = 5000)] - public void ConnectionInfo_should_write_correct_category() - { - TurboTrace.Configure(_mock); - - TurboTrace.Connection.Info(this, "test"); - - Assert.Single(_mock.Events); - Assert.Equal(TurboTraceCategory.Connection, _mock.Events[0].Category); - } - [Fact(Timeout = 5000)] public void RequestWarning_should_write_correct_level() { @@ -213,15 +194,14 @@ public void TraceCall_should_produce_no_event_when_no_listener() [Fact(Timeout = 5000)] public void CategoryFiltering_should_work_with_bitwise_flags() { - TurboTrace.Configure(_mock, TurboTraceCategory.Protocol | TurboTraceCategory.Connection); + TurboTrace.Configure(_mock, TurboTraceCategory.Protocol); TurboTrace.Protocol.Debug(this, "yes"); - TurboTrace.Connection.Debug(this, "yes"); TurboTrace.Request.Debug(this, "no"); - Assert.Equal(2, _mock.Events.Count); + Assert.Single(_mock.Events); Assert.All(_mock.Events, e => - Assert.True(e.Category == TurboTraceCategory.Protocol || e.Category == TurboTraceCategory.Connection)); + Assert.True(e.Category == TurboTraceCategory.Protocol)); } [Fact(Timeout = 5000)] @@ -240,16 +220,11 @@ public void LevelFiltering_should_work_with_minimum_level() } [Theory] - [InlineData(TurboTraceCategory.Connection)] [InlineData(TurboTraceCategory.Protocol)] [InlineData(TurboTraceCategory.Request)] - [InlineData(TurboTraceCategory.Response)] [InlineData(TurboTraceCategory.Cache)] [InlineData(TurboTraceCategory.Redirect)] [InlineData(TurboTraceCategory.Retry)] - [InlineData(TurboTraceCategory.Pool)] - [InlineData(TurboTraceCategory.Transport)] - [InlineData(TurboTraceCategory.Stream)] public void AllCategories_should_produce_correct_flag(TurboTraceCategory category) { TurboTrace.Configure(_mock, category); @@ -366,11 +341,11 @@ public void Configure_should_enable_all_categories_with_all() var categories = new[] { - TurboTraceCategory.Connection, TurboTraceCategory.Protocol, - TurboTraceCategory.Request, TurboTraceCategory.Response, - TurboTraceCategory.Cache, TurboTraceCategory.Redirect, - TurboTraceCategory.Retry, TurboTraceCategory.Pool, - TurboTraceCategory.Transport, TurboTraceCategory.Stream + TurboTraceCategory.Protocol, + TurboTraceCategory.Request, + TurboTraceCategory.Cache, + TurboTraceCategory.Redirect, + TurboTraceCategory.Retry, }; foreach (var cat in categories) @@ -411,13 +386,11 @@ public void RapidConfigureDisable_should_not_throw() [Fact(Timeout = 5000)] public void MultipleCategories_should_work_with_bitwise_or() { - var combined = TurboTraceCategory.Protocol | TurboTraceCategory.Request | TurboTraceCategory.Stream; + const TurboTraceCategory combined = TurboTraceCategory.Protocol | TurboTraceCategory.Request; TurboTrace.Configure(_mock, combined); Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Protocol, TurboTraceLevel.Debug)); Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Request, TurboTraceLevel.Debug)); - Assert.True(TurboTrace.ShouldTrace(TurboTraceCategory.Stream, TurboTraceLevel.Debug)); - Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Connection, TurboTraceLevel.Debug)); Assert.False(TurboTrace.ShouldTrace(TurboTraceCategory.Cache, TurboTraceLevel.Debug)); } @@ -425,16 +398,11 @@ private static void CallCategoryDebug(TurboTraceCategory category, object source { switch (category) { - case TurboTraceCategory.Connection: TurboTrace.Connection.Debug(source, message); break; case TurboTraceCategory.Protocol: TurboTrace.Protocol.Debug(source, message); break; case TurboTraceCategory.Request: TurboTrace.Request.Debug(source, message); break; - case TurboTraceCategory.Response: TurboTrace.Response.Debug(source, message); break; case TurboTraceCategory.Cache: TurboTrace.Cache.Debug(source, message); break; case TurboTraceCategory.Redirect: TurboTrace.Redirect.Debug(source, message); break; case TurboTraceCategory.Retry: TurboTrace.Retry.Debug(source, message); break; - case TurboTraceCategory.Pool: TurboTrace.Pool.Debug(source, message); break; - case TurboTraceCategory.Transport: TurboTrace.Transport.Debug(source, message); break; - case TurboTraceCategory.Stream: TurboTrace.Stream.Debug(source, message); break; } } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Http10/Http10StateMachineReconnectSpec.cs b/src/TurboHTTP.Tests/Http10/Http10StateMachineReconnectSpec.cs index 169090962..031ee9078 100644 --- a/src/TurboHTTP.Tests/Http10/Http10StateMachineReconnectSpec.cs +++ b/src/TurboHTTP.Tests/Http10/Http10StateMachineReconnectSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http10; using TurboHTTP.Tests.Shared; @@ -14,7 +14,7 @@ private static HttpRequestMessage MakeRequest() => public void Http10StateMachine_should_buffer_request_and_emit_reconnect_item_on_start_reconnect() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxReconnectAttempts = 3 } }); var request = MakeRequest(); sm.EncodeRequest(request); ops.Outbound.Clear(); // ignore encode output @@ -23,7 +23,7 @@ public void Http10StateMachine_should_buffer_request_and_emit_reconnect_item_on_ Assert.True(sm.IsReconnecting); Assert.False(sm.HasInFlightRequest); - Assert.Single(ops.Outbound.OfType()); + Assert.Single(ops.Outbound, item => item is ConnectItem c && c.IsReconnect); } [Fact(Timeout = 5000)] @@ -31,7 +31,7 @@ public void Http10StateMachine_should_buffer_request_and_emit_reconnect_item_on_ public void Http10StateMachine_CanAcceptRequest_should_be_false_when_reconnecting() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); @@ -43,11 +43,11 @@ public void Http10StateMachine_CanAcceptRequest_should_be_false_when_reconnectin public void Http10StateMachine_OnConnectionRestored_should_replay_buffered_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest()); ops.Outbound.Clear(); sm.StartReconnect(); - ops.Outbound.Clear(); // ignore ReconnectItem + ops.Outbound.Clear(); // ignore ConnectItem (reconnect) sm.OnConnectionRestored(); @@ -63,7 +63,7 @@ public void Http10StateMachine_OnConnectionRestored_should_replay_buffered_reque public void Http10StateMachine_OnReconnectAttemptFailed_should_fail_when_max_exceeded() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxReconnectAttempts: 1); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxReconnectAttempts = 1 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); // attempt 1 @@ -77,14 +77,14 @@ public void Http10StateMachine_OnReconnectAttemptFailed_should_fail_when_max_exc public void Http10StateMachine_OnReconnectAttemptFailed_should_emit_new_reconnect_item_when_under_limit() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); // attempt 1 - var countAfterFirst = ops.Outbound.OfType().Count(); + var countAfterFirst = ops.Outbound.OfType().Count(c => c.IsReconnect); sm.OnReconnectAttemptFailed(); // attempt 2 Assert.False(ops.ReconnectFailed); - Assert.Equal(countAfterFirst + 1, ops.Outbound.OfType().Count()); + Assert.Equal(countAfterFirst + 1, ops.Outbound.OfType().Count(c => c.IsReconnect)); } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Http10/Http10StateMachineSpec.cs b/src/TurboHTTP.Tests/Http10/Http10StateMachineSpec.cs index 719dcaefe..8b9978b44 100644 --- a/src/TurboHTTP.Tests/Http10/Http10StateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Http10/Http10StateMachineSpec.cs @@ -1,5 +1,5 @@ -using System.Net; -using TurboHTTP.Internal; +using System.Net; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http10; using TurboHTTP.Tests.Shared; @@ -7,6 +7,8 @@ namespace TurboHTTP.Tests.Http10; public sealed class Http10StateMachineSpec { + private static TurboClientOptions MakeConfig() => new(); + private static HttpRequestMessage MakeRequest(string uri = "http://example.com/", HttpContent? content = null) { var request = new HttpRequestMessage(HttpMethod.Get, uri); @@ -32,7 +34,7 @@ private static NetworkBuffer CreateResponseBuffer(string responseText) public void EncodeRequest_should_set_endpoint_on_first_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("http://example.com:8080/path")); @@ -46,7 +48,7 @@ public void EncodeRequest_should_set_endpoint_on_first_request() public void EncodeRequest_should_not_overwrite_endpoint_on_subsequent_requests() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); RequestEndpoint.FromRequest(MakeRequest("http://example.com:8080/")); sm.EncodeRequest(MakeRequest("http://example.com:8080/")); @@ -62,7 +64,7 @@ public void EncodeRequest_should_not_overwrite_endpoint_on_subsequent_requests() public void EncodeRequest_should_emit_stream_acquire_item() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); @@ -74,7 +76,7 @@ public void EncodeRequest_should_emit_stream_acquire_item() public void EncodeRequest_should_emit_network_buffer_with_encoded_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("http://example.com/test")); @@ -91,7 +93,7 @@ public void EncodeRequest_should_emit_network_buffer_with_encoded_data() public void EncodeRequest_should_set_in_flight_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); @@ -103,7 +105,7 @@ public void EncodeRequest_should_set_in_flight_request() public void EncodeRequest_should_include_content_length_in_encoded_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var content = new StringContent("hello world"); var request = MakeRequest("http://example.com/", content); @@ -122,7 +124,7 @@ public void EncodeRequest_should_calculate_buffer_size_based_on_content_length() { var ops = new FakeOps(); const int minBufferSize = 1024; - var sm = new StateMachine(ops, minBufferSize: minBufferSize); + var sm = new StateMachine(ops, MakeConfig(), minBufferSize: minBufferSize); var content = new StringContent("hello world"); var request = MakeRequest("http://example.com/", content); @@ -141,7 +143,7 @@ public void EncodeRequest_should_respect_min_buffer_size() { var ops = new FakeOps(); const int minBufferSize = 2048; - var sm = new StateMachine(ops, minBufferSize: minBufferSize); + var sm = new StateMachine(ops, MakeConfig(), minBufferSize: minBufferSize); sm.EncodeRequest(MakeRequest()); // Minimal request @@ -155,7 +157,7 @@ public void EncodeRequest_should_respect_min_buffer_size() public void EncodeRequest_should_handle_successful_encode_for_post_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var content = new StringContent("test body"); var request = new HttpRequestMessage(HttpMethod.Post, "http://example.com/api"); @@ -173,7 +175,7 @@ public void EncodeRequest_should_handle_successful_encode_for_post_request() public void EncodeRequest_should_handle_request_without_body() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var request = new HttpRequestMessage(HttpMethod.Head, "http://example.com/"); @@ -191,7 +193,7 @@ public void EncodeRequest_should_handle_request_without_body() public void DecodeServerData_should_handle_close_signal_item() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var closeSignal = new CloseSignalItem(TlsCloseKind.CleanClose); @@ -207,7 +209,7 @@ public void DecodeServerData_should_handle_close_signal_item() public void DecodeServerData_should_ignore_non_network_buffer_items() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var item = new ConnectedSignalItem { Key = default }; @@ -222,7 +224,7 @@ public void DecodeServerData_should_ignore_non_network_buffer_items() public void DecodeServerData_should_decode_complete_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var responseBuffer = CreateResponseBuffer("HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -238,7 +240,7 @@ public void DecodeServerData_should_decode_complete_response() public void DecodeServerData_should_emit_connection_reuse_item_on_successful_decode() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); ops.Outbound.Clear(); // Clear encode output @@ -254,7 +256,7 @@ public void DecodeServerData_should_emit_connection_reuse_item_on_successful_dec public void DecodeServerData_should_set_request_message_on_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var originalRequest = MakeRequest("http://example.com/test"); sm.EncodeRequest(originalRequest); @@ -272,7 +274,7 @@ public void DecodeServerData_should_set_request_message_on_response() public void DecodeServerData_should_clear_in_flight_request_on_decode() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var responseBuffer = CreateResponseBuffer("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -287,7 +289,7 @@ public void DecodeServerData_should_clear_in_flight_request_on_decode() public void DecodeServerData_should_handle_incomplete_response_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send incomplete response (missing body) @@ -304,7 +306,7 @@ public void DecodeServerData_should_handle_incomplete_response_data() public void DecodeServerData_should_dispose_buffer_after_decode() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var responseBuffer = CreateResponseBuffer("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -320,7 +322,7 @@ public void DecodeServerData_should_dispose_buffer_after_decode() public void DecodeServerData_should_handle_http09_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // HTTP/0.9 responses don't have status line — just body @@ -337,7 +339,7 @@ public void DecodeServerData_should_handle_http09_response() public void DecodeServerData_should_handle_fragmented_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send response in fragments @@ -359,7 +361,7 @@ public void DecodeServerData_should_handle_fragmented_response() public void DecodeServerData_should_throw_on_abrupt_close_with_content_length_mismatch() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // First, start receiving data with Content-Length @@ -377,7 +379,7 @@ public void DecodeServerData_should_throw_on_abrupt_close_with_content_length_mi public void DecodeServerData_should_throw_on_abrupt_close_without_content_length() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var closeSignal = new CloseSignalItem(TlsCloseKind.AbruptClose); @@ -391,7 +393,7 @@ public void DecodeServerData_should_throw_on_abrupt_close_without_content_length public void DecodeServerData_should_mark_closed_on_abrupt_close() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); try @@ -413,7 +415,7 @@ public void DecodeServerData_should_mark_closed_on_abrupt_close() public void DecodeServerData_should_handle_clean_close_with_complete_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send complete response @@ -434,7 +436,7 @@ public void DecodeServerData_should_handle_clean_close_with_complete_response() public void DecodeServerData_should_complete_response_on_clean_close_with_buffered_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send partial response that's buffered by decoder @@ -456,7 +458,7 @@ public void DecodeServerData_should_complete_response_on_clean_close_with_buffer public void DecodeServerData_should_reset_decoder_on_clean_close_with_no_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send no response data, then clean close @@ -471,7 +473,7 @@ public void DecodeServerData_should_reset_decoder_on_clean_close_with_no_data() public void TryDecodeEof_should_decode_eof_response_when_no_content_length() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Send incomplete response without Content-Length (waiting for EOF) @@ -492,7 +494,7 @@ public void TryDecodeEof_should_decode_eof_response_when_no_content_length() public void TryDecodeEof_should_return_false_when_no_buffered_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var result = sm.TryDecodeEof(); @@ -504,7 +506,7 @@ public void TryDecodeEof_should_return_false_when_no_buffered_data() public void TryDecodeEof_should_handle_http09_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // HTTP/0.9 response (no HTTP status line) @@ -524,7 +526,7 @@ public void TryDecodeEof_should_handle_http09_response() public void TryDecodeEof_should_emit_response_after_http09_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // HTTP/0.9 body data (no HTTP status line — plain text response) @@ -544,7 +546,7 @@ public void TryDecodeEof_should_emit_response_after_http09_data() public void HandleOrphanedRequest_should_warn_when_request_in_flight() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); sm.HandleOrphanedRequest(); @@ -557,7 +559,7 @@ public void HandleOrphanedRequest_should_warn_when_request_in_flight() public void HandleOrphanedRequest_should_clear_in_flight_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); sm.HandleOrphanedRequest(); @@ -570,7 +572,7 @@ public void HandleOrphanedRequest_should_clear_in_flight_request() public void HandleOrphanedRequest_should_be_noop_when_no_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.HandleOrphanedRequest(); @@ -582,7 +584,7 @@ public void HandleOrphanedRequest_should_be_noop_when_no_request() public void MarkClosed_should_prevent_new_requests() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.MarkClosed(); @@ -594,7 +596,7 @@ public void MarkClosed_should_prevent_new_requests() public void MarkClosed_should_transition_from_accepting_to_closed() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); Assert.True(sm.CanAcceptRequest); // Initially accepting @@ -608,7 +610,7 @@ public void MarkClosed_should_transition_from_accepting_to_closed() public void CanAcceptRequest_should_return_false_with_in_flight_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); Assert.False(sm.CanAcceptRequest); @@ -619,7 +621,7 @@ public void CanAcceptRequest_should_return_false_with_in_flight_request() public void CanAcceptRequest_should_return_true_when_idle() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); Assert.True(sm.CanAcceptRequest); } @@ -629,7 +631,7 @@ public void CanAcceptRequest_should_return_true_when_idle() public void PendingRequestCount_should_return_one_with_in_flight_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); Assert.Equal(1, sm.PendingRequestCount); @@ -640,7 +642,7 @@ public void PendingRequestCount_should_return_one_with_in_flight_request() public void PendingRequestCount_should_return_zero_when_idle() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); Assert.Equal(0, sm.PendingRequestCount); } @@ -650,7 +652,7 @@ public void PendingRequestCount_should_return_zero_when_idle() public void HasInFlightRequest_should_return_true_when_request_pending() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); Assert.True(sm.HasInFlightRequest); @@ -661,7 +663,7 @@ public void HasInFlightRequest_should_return_true_when_request_pending() public void HasInFlightRequest_should_return_false_when_idle() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); Assert.False(sm.HasInFlightRequest); } @@ -671,7 +673,7 @@ public void HasInFlightRequest_should_return_false_when_idle() public void Cleanup_should_clear_in_flight_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); sm.Cleanup(); @@ -684,7 +686,7 @@ public void Cleanup_should_clear_in_flight_request() public void Cleanup_should_reset_decoder() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Partially receive response @@ -706,7 +708,7 @@ public void Cleanup_should_reset_decoder() public void StateMachine_should_handle_full_request_response_cycle() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); // Encode request var request = MakeRequest("http://example.com/path"); @@ -733,7 +735,7 @@ public void StateMachine_should_handle_full_request_response_cycle() public void StateMachine_should_handle_multiple_sequential_requests() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); // First request sm.EncodeRequest(MakeRequest("http://example.com/1")); @@ -763,7 +765,7 @@ public void StateMachine_should_handle_multiple_sequential_requests() public void StateMachine_should_handle_204_no_content_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest(HttpMethod.Delete.ToString() == "DELETE" ? "http://example.com/" : "http://example.com/")); @@ -780,7 +782,7 @@ public void StateMachine_should_handle_204_no_content_response() public void StateMachine_should_handle_304_not_modified_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var responseBuffer = CreateResponseBuffer("HTTP/1.0 304 Not Modified\r\n\r\n"); @@ -795,7 +797,7 @@ public void StateMachine_should_handle_304_not_modified_response() public void StateMachine_should_allow_request_after_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var responseBuffer = CreateResponseBuffer("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -809,7 +811,7 @@ public void StateMachine_should_allow_request_after_response() public void StateMachine_should_preserve_request_reference_across_responses() { var ops = new FakeOps(); - var sm = new StateMachine(ops); + var sm = new StateMachine(ops, MakeConfig()); var request1 = MakeRequest("http://example.com/path1"); sm.EncodeRequest(request1); diff --git a/src/TurboHTTP.Tests/Http10/RoundTripHeaderSpec.cs b/src/TurboHTTP.Tests/Http10/RoundTripHeaderSpec.cs index 82800d584..0b80ffd49 100644 --- a/src/TurboHTTP.Tests/Http10/RoundTripHeaderSpec.cs +++ b/src/TurboHTTP.Tests/Http10/RoundTripHeaderSpec.cs @@ -1,6 +1,5 @@ using System.Text; using Decoder = TurboHTTP.Protocol.Http10.Decoder; -using Encoder = TurboHTTP.Protocol.Http10.Encoder; namespace TurboHTTP.Tests.Http10; diff --git a/src/TurboHTTP.Tests/Http11/Http11StateMachineReconnectSpec.cs b/src/TurboHTTP.Tests/Http11/Http11StateMachineReconnectSpec.cs index 008299b8c..df0af8b7d 100644 --- a/src/TurboHTTP.Tests/Http11/Http11StateMachineReconnectSpec.cs +++ b/src/TurboHTTP.Tests/Http11/Http11StateMachineReconnectSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http11; using TurboHTTP.Tests.Shared; @@ -17,7 +17,7 @@ private static HttpRequestMessage MakeRequest(string path = "/") => public void Http11StateMachine_should_buffer_all_inflight_requests_and_emit_reconnect_item_on_start_reconnect() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 4, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxPipelineDepth = 4, MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest("/a")); sm.EncodeRequest(MakeRequest("/b")); ops.Outbound.Clear(); @@ -26,7 +26,7 @@ public void Http11StateMachine_should_buffer_all_inflight_requests_and_emit_reco Assert.True(sm.IsReconnecting); Assert.False(sm.HasInFlightRequests); // queue drained into buffer - Assert.Single(ops.Outbound.OfType()); + Assert.Single(ops.Outbound, item => item is ConnectItem c && c.IsReconnect); } [Fact(Timeout = 5000)] @@ -34,7 +34,7 @@ public void Http11StateMachine_should_buffer_all_inflight_requests_and_emit_reco public void Http11StateMachine_CanAcceptRequest_should_be_false_when_reconnecting() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 4, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxPipelineDepth = 4, MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); @@ -46,12 +46,12 @@ public void Http11StateMachine_CanAcceptRequest_should_be_false_when_reconnectin public void Http11StateMachine_OnConnectionRestored_should_replay_all_buffered_requests() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 4, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxPipelineDepth = 4, MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest("/a")); sm.EncodeRequest(MakeRequest("/b")); ops.Outbound.Clear(); sm.StartReconnect(); - ops.Outbound.Clear(); // ignore ReconnectItem + ops.Outbound.Clear(); // ignore ConnectItem (reconnect) sm.OnConnectionRestored(); @@ -67,7 +67,7 @@ public void Http11StateMachine_OnConnectionRestored_should_replay_all_buffered_r public void Http11StateMachine_OnReconnectAttemptFailed_should_fail_when_max_exceeded() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 4, maxReconnectAttempts: 1); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxPipelineDepth = 4, MaxReconnectAttempts = 1 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); // attempt 1 @@ -81,14 +81,14 @@ public void Http11StateMachine_OnReconnectAttemptFailed_should_fail_when_max_exc public void Http11StateMachine_OnReconnectAttemptFailed_should_emit_new_reconnect_item_when_under_limit() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 4, maxReconnectAttempts: 3); + var sm = new StateMachine(ops, new TurboClientOptions { Http1 = new Http1Options { MaxPipelineDepth = 4, MaxReconnectAttempts = 3 } }); sm.EncodeRequest(MakeRequest()); sm.StartReconnect(); // attempt 1 - var countAfterFirst = ops.Outbound.OfType().Count(); + var countAfterFirst = ops.Outbound.OfType().Count(c => c.IsReconnect); sm.OnReconnectAttemptFailed(); // attempt 2 Assert.False(ops.ReconnectFailed); - Assert.Equal(countAfterFirst + 1, ops.Outbound.OfType().Count()); + Assert.Equal(countAfterFirst + 1, ops.Outbound.OfType().Count(c => c.IsReconnect)); } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Http11/Http11StateMachineSpec.cs b/src/TurboHTTP.Tests/Http11/Http11StateMachineSpec.cs index 537ba4131..d0cce91ec 100644 --- a/src/TurboHTTP.Tests/Http11/Http11StateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Http11/Http11StateMachineSpec.cs @@ -1,5 +1,5 @@ -using System.Text; -using TurboHTTP.Internal; +using System.Text; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http11; using TurboHTTP.Tests.Shared; @@ -7,6 +7,8 @@ namespace TurboHTTP.Tests.Http11; public sealed class Http11StateMachineSpec { + private static TurboClientOptions MakeConfig(int maxPipelineDepth = 8) => new() { Http1 = new() { MaxPipelineDepth = maxPipelineDepth } }; + private static HttpRequestMessage MakeRequest(string path = "/", string? method = null, HttpContent? content = null) { var httpMethod = method switch @@ -42,7 +44,7 @@ private static NetworkBuffer CreateResponseBuffer(string response) public void EncodeRequest_should_enqueue_request_and_emit_stream_acquire() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); @@ -55,7 +57,7 @@ public void EncodeRequest_should_enqueue_request_and_emit_stream_acquire() public void EncodeRequest_should_emit_network_buffer_with_encoded_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); @@ -70,7 +72,7 @@ public void EncodeRequest_should_emit_network_buffer_with_encoded_data() public void EncodeRequest_should_set_endpoint_on_first_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); @@ -82,7 +84,7 @@ public void EncodeRequest_should_set_endpoint_on_first_request() public void EncodeRequest_should_respect_max_pipeline_depth() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 2); + var sm = new StateMachine(ops, MakeConfig(maxPipelineDepth: 2)); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -95,7 +97,7 @@ public void EncodeRequest_should_respect_max_pipeline_depth() public void EncodeRequest_should_handle_post_request_with_content() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); var content = new StringContent("test body", Encoding.UTF8); sm.EncodeRequest(MakeRequest("/", "POST", content)); @@ -113,7 +115,7 @@ public void EncodeRequest_should_handle_post_request_with_content() public void EncodeRequest_should_emit_multiple_requests_in_pipeline() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -133,7 +135,7 @@ public void EncodeRequest_should_emit_multiple_requests_in_pipeline() public void EncodeRequest_should_handle_request_without_content() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/", "GET")); @@ -148,7 +150,7 @@ public void EncodeRequest_should_handle_request_without_content() public void EncodeRequest_should_respect_max_buffer_size() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8, minBufferSize: 1024, maxBufferSize: 2048); + var sm = new StateMachine(ops, MakeConfig(), minBufferSize: 1024, maxBufferSize: 2048); var content = new StringContent("test", Encoding.UTF8); sm.EncodeRequest(MakeRequest("/", "POST", content)); @@ -164,7 +166,7 @@ public void EncodeRequest_should_respect_max_buffer_size() public void DecodeServerData_should_decode_single_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"); @@ -179,7 +181,7 @@ public void DecodeServerData_should_decode_single_response() public void DecodeServerData_should_emit_connection_reuse_item() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -193,7 +195,7 @@ public void DecodeServerData_should_emit_connection_reuse_item() public void DecodeServerData_should_decode_multiple_pipelined_responses() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -212,7 +214,7 @@ public void DecodeServerData_should_decode_multiple_pipelined_responses() public void DecodeServerData_should_buffer_close_delimited_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Response with no Content-Length or Transfer-Encoding (close-delimited) @@ -229,7 +231,7 @@ public void DecodeServerData_should_buffer_close_delimited_response() public void DecodeServerData_should_accumulate_body_for_close_delimited_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // First chunk: headers without Content-Length @@ -249,7 +251,7 @@ public void DecodeServerData_should_accumulate_body_for_close_delimited_response public void DecodeServerData_should_handle_connection_close_header() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -267,7 +269,7 @@ public void DecodeServerData_should_handle_connection_close_header() public void DecodeServerData_should_handle_close_signal_items() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); sm.DecodeServerData(buffer); @@ -284,7 +286,7 @@ public void DecodeServerData_should_handle_close_signal_items() public void DecodeServerData_should_clear_effective_pipeline_depth_when_connection_close_with_multiple_inflight() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); sm.EncodeRequest(MakeRequest("/3")); @@ -301,7 +303,7 @@ public void DecodeServerData_should_clear_effective_pipeline_depth_when_connecti public void DecodeServerData_should_preserve_request_reference() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); var req = MakeRequest(); sm.EncodeRequest(req); @@ -316,7 +318,7 @@ public void DecodeServerData_should_preserve_request_reference() public void HandleCloseSignal_should_complete_close_delimited_response_on_clean_close() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Setup close-delimited response @@ -337,7 +339,7 @@ public void HandleCloseSignal_should_complete_close_delimited_response_on_clean_ public void HandleCloseSignal_should_throw_on_abrupt_close_with_pending_close_delimited() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\n\r\n"); @@ -354,7 +356,7 @@ public void HandleCloseSignal_should_throw_on_abrupt_close_with_pending_close_de public void HandleCloseSignal_should_decode_eof_response_on_clean_close() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Incomplete response (no final headers delimiter) @@ -372,7 +374,7 @@ public void HandleCloseSignal_should_decode_eof_response_on_clean_close() public void HandleCloseSignal_should_warn_on_abrupt_close_without_pending() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -388,7 +390,7 @@ public void HandleCloseSignal_should_warn_on_abrupt_close_without_pending() public void HandleCloseSignal_should_dispose_body_owners_on_abrupt_close() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer1 = CreateResponseBuffer("HTTP/1.1 200 OK\r\n\r\n"); @@ -407,7 +409,7 @@ public void HandleCloseSignal_should_dispose_body_owners_on_abrupt_close() public void HandleCloseSignal_should_handle_clean_close_without_buffered_response() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -423,7 +425,7 @@ public void HandleCloseSignal_should_handle_clean_close_without_buffered_respons public void TryDecodeEof_should_return_false_when_no_buffered_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); var result = sm.TryDecodeEof(); @@ -435,7 +437,7 @@ public void TryDecodeEof_should_return_false_when_no_buffered_data() public void TryDecodeEof_should_complete_response_when_buffered_data() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Use a response without final \r\n to leave incomplete data in decoder buffer @@ -453,7 +455,7 @@ public void TryDecodeEof_should_complete_response_when_buffered_data() public void TryDecodeEof_should_return_false_on_exception() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); // No setup needed, invalid data will cause exception which is caught var result = sm.TryDecodeEof(); @@ -466,7 +468,7 @@ public void TryDecodeEof_should_return_false_on_exception() public void TryDecodeEof_should_reset_decoder_after_decode() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer = CreateResponseBuffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -483,7 +485,7 @@ public void TryDecodeEof_should_reset_decoder_after_decode() public void HandleOrphanedRequests_should_clear_queue_when_inflight() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -498,7 +500,7 @@ public void HandleOrphanedRequests_should_clear_queue_when_inflight() public void HandleOrphanedRequests_should_disable_pipelining() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); sm.HandleOrphanedRequests(); @@ -513,7 +515,7 @@ public void HandleOrphanedRequests_should_disable_pipelining() public void HandleOrphanedRequests_should_return_early_when_empty() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.HandleOrphanedRequests(); @@ -525,7 +527,7 @@ public void HandleOrphanedRequests_should_return_early_when_empty() public void CanAcceptRequest_should_be_true_initially() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); Assert.True(sm.CanAcceptRequest); } @@ -535,7 +537,7 @@ public void CanAcceptRequest_should_be_true_initially() public void CanAcceptRequest_should_be_false_when_queue_full() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 2); + var sm = new StateMachine(ops, MakeConfig(maxPipelineDepth: 2)); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -547,7 +549,7 @@ public void CanAcceptRequest_should_be_false_when_queue_full() public void HasInFlightRequests_should_reflect_queue_count() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); Assert.False(sm.HasInFlightRequests); sm.EncodeRequest(MakeRequest()); @@ -559,7 +561,7 @@ public void HasInFlightRequests_should_reflect_queue_count() public void Endpoint_should_be_initialized_on_first_request() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); Assert.Equal(default, sm.Endpoint); sm.EncodeRequest(MakeRequest()); @@ -571,7 +573,7 @@ public void Endpoint_should_be_initialized_on_first_request() public void PendingRequestCount_should_reflect_queue_count() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -583,7 +585,7 @@ public void PendingRequestCount_should_reflect_queue_count() public void IsReconnecting_should_be_false_initially() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); Assert.False(sm.IsReconnecting); } @@ -593,7 +595,7 @@ public void IsReconnecting_should_be_false_initially() public void Cleanup_should_clear_inflight_queue() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); @@ -607,7 +609,7 @@ public void Cleanup_should_clear_inflight_queue() public void Cleanup_should_dispose_body_owners() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); var buffer1 = CreateResponseBuffer("HTTP/1.1 200 OK\r\n\r\n"); @@ -626,7 +628,7 @@ public void Cleanup_should_dispose_body_owners() public void Pipeline_should_correlate_responses_to_requests_in_order() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); sm.EncodeRequest(MakeRequest("/3")); @@ -648,7 +650,7 @@ public void Pipeline_should_correlate_responses_to_requests_in_order() public void CloseDelimited_should_work_with_initial_body_bytes() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Response headers and partial body in one buffer @@ -671,7 +673,7 @@ public void CloseDelimited_should_work_with_initial_body_bytes() public void NoBodyResponseTypes_should_not_be_close_delimited() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // 204 No Content (should complete immediately) @@ -687,7 +689,7 @@ public void NoBodyResponseTypes_should_not_be_close_delimited() public void Not_Modified_should_not_be_close_delimited() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // 304 Not Modified (should complete immediately) @@ -703,7 +705,7 @@ public void Not_Modified_should_not_be_close_delimited() public void TransferEncoding_chunked_should_not_be_close_delimited() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest()); // Response with chunked encoding (not close-delimited) @@ -719,7 +721,7 @@ public void TransferEncoding_chunked_should_not_be_close_delimited() public void Multiple_requests_with_connection_close_should_disable_pipeline() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.EncodeRequest(MakeRequest("/1")); sm.EncodeRequest(MakeRequest("/2")); sm.EncodeRequest(MakeRequest("/3")); @@ -738,7 +740,7 @@ public void Multiple_requests_with_connection_close_should_disable_pipeline() public void Empty_request_queue_and_orphaned_should_not_warn() { var ops = new FakeOps(); - var sm = new StateMachine(ops, maxPipelineDepth: 8); + var sm = new StateMachine(ops, MakeConfig()); sm.HandleOrphanedRequests(); diff --git a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineKeepAliveSpec.cs b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineKeepAliveSpec.cs index f4abdf29c..b187492c3 100644 --- a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineKeepAliveSpec.cs +++ b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineKeepAliveSpec.cs @@ -1,25 +1,12 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http2.Connection; public sealed class Http2StateMachineKeepAliveSpec { - private static Http2EngineOptions MakeConfig() => - new( - MaxConnectionsPerServer: 6, - InitialConcurrentStreams: 100, - InitialConnectionWindowSize: 65535, - InitialStreamWindowSize: 65535, - MaxFrameSize: 16384, - HeaderTableSize: 4096, - MaxReconnectAttempts: 3, - MaxBatchWeight: 262_144, - KeepAlivePingDelay: TimeSpan.FromSeconds(5), - KeepAlivePingTimeout: TimeSpan.FromSeconds(20), - KeepAlivePingPolicy: HttpKeepAlivePingPolicy.Always); + private static TurboClientOptions MakeConfig() => new(); [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-6.7")] diff --git a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineReconnectSpec.cs b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineReconnectSpec.cs index af778268f..5ef00c00b 100644 --- a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineReconnectSpec.cs +++ b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineReconnectSpec.cs @@ -1,25 +1,18 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http2.Connection; public sealed class Http2StateMachineReconnectSpec { - private static Http2EngineOptions MakeConfig(int maxReconnect = 3) => - new( - MaxConnectionsPerServer: 6, - InitialConcurrentStreams: 100, - InitialConnectionWindowSize: 65535, - InitialStreamWindowSize: 65535, - MaxFrameSize: 16384, - HeaderTableSize: 4096, - MaxReconnectAttempts: maxReconnect, - MaxBatchWeight: 262_144, - KeepAlivePingDelay: Timeout.InfiniteTimeSpan, - KeepAlivePingTimeout: TimeSpan.FromSeconds(20), - KeepAlivePingPolicy: HttpKeepAlivePingPolicy.Always); + private static TurboClientOptions MakeConfig(int? maxConcurrentStreams = null, int? maxReconnect = null) + { + var options = new TurboClientOptions(); + if (maxConcurrentStreams.HasValue) options.Http2.MaxConcurrentStreams = maxConcurrentStreams.Value; + if (maxReconnect.HasValue) options.Http2.MaxReconnectAttempts = maxReconnect.Value; + return options; + } private static HttpRequestMessage MakeGet(string path = "/") => new(HttpMethod.Get, $"https://example.com{path}"); @@ -44,7 +37,7 @@ public void Http2StateMachine_OnConnectionLost_should_buffer_streams_above_lastS Assert.True(sm.IsReconnecting); Assert.Equal(2, sm.ReconnectBufferCount); - Assert.Single(ops.Outbound.OfType()); + Assert.Single(ops.Outbound, item => item is ConnectItem c && c.IsReconnect); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineSpec.cs b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineSpec.cs index abaf0b5cd..ceeb069c3 100644 --- a/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Http2/Connection/Http2StateMachineSpec.cs @@ -1,26 +1,19 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http2; using TurboHTTP.Protocol.Http2.Hpack; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http2.Connection; public sealed class Http2StateMachineSpec { - private static Http2EngineOptions MakeConfig(int maxReconnect = 3, int maxConcurrentStreams = 100) => - new( - MaxConnectionsPerServer: 6, - InitialConcurrentStreams: maxConcurrentStreams, - InitialConnectionWindowSize: 65535, - InitialStreamWindowSize: 65535, - MaxFrameSize: 16384, - HeaderTableSize: 4096, - MaxReconnectAttempts: maxReconnect, - MaxBatchWeight: 262_144, - KeepAlivePingDelay: Timeout.InfiniteTimeSpan, - KeepAlivePingTimeout: TimeSpan.FromSeconds(20), - KeepAlivePingPolicy: HttpKeepAlivePingPolicy.Always); + private static TurboClientOptions MakeConfig(int? maxConcurrentStreams = null, int? maxReconnect = null) + { + var options = new TurboClientOptions(); + if (maxConcurrentStreams.HasValue) options.Http2.MaxConcurrentStreams = maxConcurrentStreams.Value; + if (maxReconnect.HasValue) options.Http2.MaxReconnectAttempts = maxReconnect.Value; + return options; + } private static HttpRequestMessage MakeGet(string path = "/") => new(HttpMethod.Get, $"https://example.com{path}"); @@ -76,18 +69,23 @@ public void StateMachine_TryBuildPreface_should_return_null_on_subsequent_calls( public void StateMachine_TryBuildPreface_should_return_null_when_connection_window_disabled() { var ops = new FakeOps(); - var config = new Http2EngineOptions( - MaxConnectionsPerServer: 6, - InitialConcurrentStreams: 100, - InitialConnectionWindowSize: 0, // disabled - InitialStreamWindowSize: 65535, - MaxFrameSize: 16384, - HeaderTableSize: 4096, - MaxReconnectAttempts: 3, - MaxBatchWeight: 262_144, - KeepAlivePingDelay: Timeout.InfiniteTimeSpan, - KeepAlivePingTimeout: TimeSpan.FromSeconds(20), - KeepAlivePingPolicy: HttpKeepAlivePingPolicy.Always); + var config = new TurboClientOptions + { + Http2 = new Http2Options + { + MaxConnectionsPerServer = 6, + MaxConcurrentStreams = 100, + InitialConnectionWindowSize = 0, // disabled + InitialStreamWindowSize = 65535, + MaxFrameSize = 16384, + HeaderTableSize = 4096, + MaxReconnectAttempts = 3, + MaxBatchWeight = 262_144, + KeepAlivePingDelay = Timeout.InfiniteTimeSpan, + KeepAlivePingTimeout = TimeSpan.FromSeconds(20), + KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always + } + }; var sm = new StateMachine(config, ops); var preface = sm.TryBuildPreface(); @@ -494,7 +492,7 @@ public void StateMachine_ProcessFrame_should_trigger_reconnect_on_goaway_with_in sm.ProcessFrame(goaway); Assert.True(sm.IsReconnecting); - Assert.Single(ops.Outbound.OfType()); + Assert.Single(ops.Outbound, item => item is ConnectItem c && c.IsReconnect); } [Fact(Timeout = 5000)] @@ -774,7 +772,7 @@ public void StateMachine_OnReconnectAttemptFailed_should_emit_new_reconnect_when sm.OnReconnectAttemptFailed(); // attempt 2 Assert.True(sm.IsReconnecting); - Assert.Single(ops.Outbound.OfType()); + Assert.Single(ops.Outbound, item => item is ConnectItem c && c.IsReconnect); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Http2/FlowControl/ResourceExhaustionPart2Spec.cs b/src/TurboHTTP.Tests/Http2/FlowControl/ResourceExhaustionPart2Spec.cs index b52b17a00..2f6510d3b 100644 --- a/src/TurboHTTP.Tests/Http2/FlowControl/ResourceExhaustionPart2Spec.cs +++ b/src/TurboHTTP.Tests/Http2/FlowControl/ResourceExhaustionPart2Spec.cs @@ -69,7 +69,7 @@ public void HpackDecoder_should_keep_dynamic_table_within_limit_when_adding_many }; fullBlock.AddRange(blocks); - hpack.Decode([..fullBlock]); // must not throw; eviction must have maintained bounds + hpack.Decode([.. fullBlock]); // must not throw; eviction must have maintained bounds } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Http2/Hpack/DynamicTableSpec.cs b/src/TurboHTTP.Tests/Http2/Hpack/DynamicTableSpec.cs index 42e427157..c704d667d 100644 --- a/src/TurboHTTP.Tests/Http2/Hpack/DynamicTableSpec.cs +++ b/src/TurboHTTP.Tests/Http2/Hpack/DynamicTableSpec.cs @@ -154,18 +154,18 @@ public void HpackDynamicTable_should_remove_oldest_entry_first_when_eviction_occ { var table = new HpackDynamicTable(); table.Add("alpha", "1"); - table.Add("beta", "2"); + table.Add("beta", "2"); table.Add("gamma", "3"); var gammaSize = "gamma".Length + "3".Length + 32; - var betaSize = "beta".Length + "2".Length + 32; + var betaSize = "beta".Length + "2".Length + 32; var newMax = gammaSize + betaSize; table.SetMaxSize(newMax); Assert.Equal(2, table.Count); Assert.Equal("gamma", table.GetEntry(1)!.Value.Name); - Assert.Equal("beta", table.GetEntry(2)!.Value.Name); + Assert.Equal("beta", table.GetEntry(2)!.Value.Name); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Http2/Security/FuzzHarnessPart2Spec.cs b/src/TurboHTTP.Tests/Http2/Security/FuzzHarnessPart2Spec.cs index 5301e897f..5de5026ac 100644 --- a/src/TurboHTTP.Tests/Http2/Security/FuzzHarnessPart2Spec.cs +++ b/src/TurboHTTP.Tests/Http2/Security/FuzzHarnessPart2Spec.cs @@ -351,22 +351,22 @@ public void Http2FrameDecoder_should_survive_extended_random_frame_sequence_with break; case 4: // Random garbage HEADERS on a new stream - { - var garbage = new byte[rng.Next(0, 64)]; - rng.NextBytes(garbage); - AssertDecodeNeverCrashes(decoder, BuildHeadersFrame(streamCounter, garbage)); - streamCounter += 2; // Advance to next valid odd client stream ID - break; - } + { + var garbage = new byte[rng.Next(0, 64)]; + rng.NextBytes(garbage); + AssertDecodeNeverCrashes(decoder, BuildHeadersFrame(streamCounter, garbage)); + streamCounter += 2; // Advance to next valid odd client stream ID + break; + } case 5: // RST_STREAM on a random previous stream - { - var targetStream = streamCounter > 1 ? rng.Next(1, streamCounter) : 1; - var payload = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(payload, (uint)rng.Next(0, 20)); - AssertDecodeNeverCrashes(decoder, BuildRawFrame(0x3, 0, targetStream, payload)); - break; - } + { + var targetStream = streamCounter > 1 ? rng.Next(1, streamCounter) : 1; + var payload = new byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(payload, (uint)rng.Next(0, 20)); + AssertDecodeNeverCrashes(decoder, BuildRawFrame(0x3, 0, targetStream, payload)); + break; + } } } } diff --git a/src/TurboHTTP.Tests/Http3/Connection/Http3DecoderStreamSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/Http3DecoderStreamSpec.cs index 16c826e42..d2a268460 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/Http3DecoderStreamSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/Http3DecoderStreamSpec.cs @@ -1,6 +1,5 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http3.Connection; @@ -9,9 +8,9 @@ public sealed class Http3DecoderStreamSpec { private readonly FakeOps _ops = new(); - private StateMachine CreateMachine(Http3EngineOptions? options = null) + private StateMachine CreateMachine(TurboClientOptions? options = null) { - return new StateMachine(options ?? new Http3Options().ToEngineOptions(), _ops); + return new StateMachine(options ?? new TurboClientOptions(), _ops); } [Fact(Timeout = 5000)] @@ -22,8 +21,8 @@ public void FlushDecoderInstructions_should_not_emit_when_no_instructions_pendin sm.FlushDecoderInstructions(); var decoderItems = _ops.Outbound - .OfType() - .Where(t => t.StreamType == Http3StreamType.QpackDecoder) + .OfType() + .Where(t => t.StreamTypeValue == (long)StreamType.QpackDecoder) .ToList(); Assert.Empty(decoderItems); } @@ -41,8 +40,8 @@ public void FlushDecoderInstructions_should_prepend_stream_type_on_first_emissio sm.FlushDecoderInstructions(); var decoderItems = _ops.Outbound - .OfType() - .Where(t => t.StreamType == Http3StreamType.QpackDecoder) + .OfType() + .Where(t => t.StreamTypeValue == (long)StreamType.QpackDecoder) .ToList(); Assert.Single(decoderItems); @@ -125,8 +124,8 @@ public void ProcessQpackEncoderBytes_should_emit_insert_count_increment() sm.ProcessQpackEncoderBytes(encoderInstr); var decoderItems = _ops.Outbound - .OfType() - .Where(t => t.StreamType == Http3StreamType.QpackDecoder) + .OfType() + .Where(t => t.StreamTypeValue == (long)StreamType.QpackDecoder) .ToList(); Assert.Single(decoderItems); @@ -139,8 +138,8 @@ public void ProcessQpackEncoderBytes_should_emit_insert_count_increment() private static NetworkBuffer ExtractDecoderBuffer(FakeOps ops, int index) { var items = ops.Outbound - .OfType() - .Where(t => t.StreamType == Http3StreamType.QpackDecoder) + .OfType() + .Where(t => t.StreamTypeValue == (long)StreamType.QpackDecoder) .ToList(); return items[index]; } diff --git a/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineEdgeCasesSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineEdgeCasesSpec.cs index 7ac6a284e..770643def 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineEdgeCasesSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineEdgeCasesSpec.cs @@ -1,6 +1,5 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http3.Connection; @@ -10,11 +9,11 @@ public sealed class Http3StateMachineEdgeCasesSpec private readonly FakeOps _ops = new(); private StateMachine CreateMachine( - Http3EngineOptions? options = null, + TurboClientOptions? options = null, FakeOps? ops = null) { return new StateMachine( - options ?? new Http3Options().ToEngineOptions(), + options ?? new TurboClientOptions(), ops ?? _ops); } @@ -27,9 +26,9 @@ public void TryBuildControlPreface_should_emit_preface_on_first_call() var preface = sm.TryBuildControlPreface(); Assert.NotNull(preface); - Assert.IsType(preface); - var buf = (Http3NetworkBuffer)preface; - Assert.Equal(Http3StreamType.Control, buf.StreamType); + Assert.IsType(preface); + var buf = (RoutedNetworkBuffer)preface; + Assert.Equal((long)StreamType.Control, buf.StreamTypeValue); Assert.True(buf.Length > 0); } @@ -50,15 +49,15 @@ public void TryBuildControlPreface_should_return_null_on_subsequent_calls() [Trait("RFC", "RFC9114-6.2")] public void TryBuildControlPreface_should_include_max_push_id_when_push_enabled() { - var sm = CreateMachine(new Http3Options { AllowServerPush = true }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = true } }); var preface = sm.TryBuildControlPreface(); Assert.NotNull(preface); - var buf = (Http3NetworkBuffer)preface; + var buf = (RoutedNetworkBuffer)preface; // Preface contains: StreamType VarInt + Settings frame + MaxPushIdFrame // With MAX_PUSH_ID, size should be larger than without it - Assert.Equal(Http3StreamType.Control, buf.StreamType); + Assert.Equal((long)StreamType.Control, buf.StreamTypeValue); Assert.True(buf.Length > 0); } @@ -66,14 +65,14 @@ public void TryBuildControlPreface_should_include_max_push_id_when_push_enabled( [Trait("RFC", "RFC9114-6.2")] public void TryBuildControlPreface_should_not_include_max_push_id_when_push_disabled() { - var sm = CreateMachine(new Http3Options { AllowServerPush = false }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = false } }); var preface = sm.TryBuildControlPreface(); Assert.NotNull(preface); - var buf = (Http3NetworkBuffer)preface; + var buf = (RoutedNetworkBuffer)preface; // Without MaxPushIdFrame, still contains StreamType VarInt + Settings frame - Assert.Equal(Http3StreamType.Control, buf.StreamType); + Assert.Equal((long)StreamType.Control, buf.StreamTypeValue); Assert.True(buf.Length > 0); } @@ -86,8 +85,8 @@ public void TryBuildControlPreface_should_emit_via_outbound_callback_after_recon sm.OnConnectionRestored(); // OnConnectionRestored emits preface via _ops callback - var prefaces = _ops.Outbound.OfType() - .Where(b => b.StreamType == Http3StreamType.Control) + var prefaces = _ops.Outbound.OfType() + .Where(b => b.StreamTypeValue == (long)StreamType.Control) .ToList(); Assert.NotEmpty(prefaces); } @@ -97,7 +96,7 @@ public void TryBuildControlPreface_should_emit_via_outbound_callback_after_recon public void DecodeServerData_should_delegate_to_stream_manager() { var sm = CreateMachine(); - var buffer = Http3NetworkBuffer.Rent(10); + var buffer = RoutedNetworkBuffer.Rent(10); buffer.FullMemory.Span[..1].Fill(0x00); // minimal DATA frame buffer.Length = 1; @@ -113,11 +112,11 @@ public void DecodeServerData_should_decode_multiple_stream_ids() { var sm = CreateMachine(); - var buffer1 = Http3NetworkBuffer.Rent(1); + var buffer1 = RoutedNetworkBuffer.Rent(1); buffer1.FullMemory.Span[0] = 0x00; buffer1.Length = 1; - var buffer4 = Http3NetworkBuffer.Rent(1); + var buffer4 = RoutedNetworkBuffer.Rent(1); buffer4.FullMemory.Span[0] = 0x00; buffer4.Length = 1; @@ -176,7 +175,7 @@ public void IsTimeoutDisabled_should_be_false_unless_explicitly_disabled() { // StateMachine replaces zero timeout with DefaultIdleTimeout (30s) // so IsTimeoutDisabled is never true in normal operation - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromSeconds(1) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromSeconds(1) } }); Assert.False(sm.IsTimeoutDisabled); } @@ -185,7 +184,7 @@ public void IsTimeoutDisabled_should_be_false_unless_explicitly_disabled() [Trait("RFC", "RFC9114-5.1")] public void IsTimeoutDisabled_should_be_false_for_nonzero_timeout() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromSeconds(30) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromSeconds(30) } }); Assert.False(sm.IsTimeoutDisabled); } @@ -194,7 +193,7 @@ public void IsTimeoutDisabled_should_be_false_for_nonzero_timeout() [Trait("RFC", "RFC9114-5.1")] public void TimeUntilExpiry_should_return_remaining_time() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromSeconds(10) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromSeconds(10) } }); var remaining = sm.TimeUntilExpiry(); @@ -206,7 +205,7 @@ public void TimeUntilExpiry_should_return_remaining_time() [Trait("RFC", "RFC9114-5.1")] public void TimeUntilExpiry_should_return_remaining_time_on_active_connection() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromSeconds(60) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromSeconds(60) } }); var remaining = sm.TimeUntilExpiry(); @@ -404,9 +403,9 @@ public void OnConnectionRestored_should_emit_preface_before_replaying() // First item should be control preface var items = _ops.Outbound.ToList(); Assert.NotEmpty(items); - if (items[0] is Http3NetworkBuffer buf) + if (items[0] is RoutedNetworkBuffer buf) { - Assert.Equal(Http3StreamType.Control, buf.StreamType); + Assert.Equal((long)StreamType.Control, buf.StreamTypeValue); } } @@ -445,7 +444,7 @@ public void OnConnectionRestored_should_clear_reconnect_buffer() [Trait("RFC", "RFC9114-5")] public void OnReconnectAttemptFailed_should_track_attempts_separately() { - var options = new Http3Options { MaxReconnectAttempts = 5 }.ToEngineOptions(); + var options = new TurboClientOptions { Http3 = new Http3Options { MaxReconnectAttempts = 5 } }; var sm = CreateMachine(options); sm.OnConnectionLost(); @@ -470,7 +469,7 @@ public void CanAcceptRequest_should_be_false_during_first_reconnect_attempt() [Trait("RFC", "RFC9114-6")] public async Task ProcessFrame_should_record_activity_on_all_frames() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(50) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(50) } }); await Task.Delay(100, TestContext.Current.CancellationToken); diff --git a/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineSpec.cs index 175ebe31f..fdd2be7f3 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineSpec.cs @@ -1,6 +1,5 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; -using TurboHTTP.Streams; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Http3.Connection; @@ -10,11 +9,11 @@ public sealed class Http3StateMachineSpec private readonly FakeOps _ops = new(); private StateMachine CreateMachine( - Http3EngineOptions? options = null, + TurboClientOptions? options = null, FakeOps? ops = null) { return new StateMachine( - options ?? new Http3Options().ToEngineOptions(), + options ?? new TurboClientOptions(), ops ?? _ops); } @@ -118,20 +117,20 @@ public void ProcessFrame_should_warn_on_non_divisible_by_four_goaway() [Fact(Timeout = 5000)] public void ProcessFrame_should_reject_push_promise_when_push_disabled() { - var sm = CreateMachine(new Http3Options { AllowServerPush = false }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = false } }); var push = new Http3PushPromiseFrame(1, new byte[] { 0x01 }); var result = sm.ProcessFrame(push); Assert.Null(result); Assert.Single(_ops.Outbound); // serialized CANCEL_PUSH frame - Assert.IsType(_ops.Outbound[0]); + Assert.IsType(_ops.Outbound[0]); } [Fact(Timeout = 5000)] public void ProcessFrame_should_warn_when_push_rejected() { - var sm = CreateMachine(new Http3Options { AllowServerPush = false }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = false } }); sm.ProcessFrame(new Http3PushPromiseFrame(42, new byte[] { 0x01 })); @@ -141,7 +140,7 @@ public void ProcessFrame_should_warn_when_push_rejected() [Fact(Timeout = 5000)] public void ProcessFrame_should_forward_push_promise_to_app_when_push_enabled() { - var sm = CreateMachine(new Http3Options { AllowServerPush = true }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = true } }); var push = new Http3PushPromiseFrame(1, new byte[] { 0x01 }); var result = sm.ProcessFrame(push); @@ -153,7 +152,7 @@ public void ProcessFrame_should_forward_push_promise_to_app_when_push_enabled() [Fact(Timeout = 5000)] public void ProcessFrame_should_enforce_push_limit_when_push_enabled() { - var sm = CreateMachine(new Http3Options { AllowServerPush = true }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { AllowServerPush = true } }); // The default maxPushCount is 100 when AllowServerPush = true. // Push 100 times to hit the limit, then one more should warn. @@ -301,7 +300,7 @@ public void CheckIdleTimeout_should_return_null_when_not_expired() [Fact(Timeout = 5000)] public void CheckIdleTimeout_should_return_null_when_timeout_disabled() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.Zero }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.Zero } }); var result = sm.CheckIdleTimeout(); @@ -311,7 +310,7 @@ public void CheckIdleTimeout_should_return_null_when_timeout_disabled() [Fact(Timeout = 5000)] public async Task CheckIdleTimeout_should_return_goaway_when_expired_no_active_streams() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(1) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(1) } }); await Task.Delay(20, TestContext.Current.CancellationToken); @@ -324,7 +323,7 @@ public async Task CheckIdleTimeout_should_return_goaway_when_expired_no_active_s [Fact(Timeout = 5000)] public async Task CheckIdleTimeout_should_not_expire_when_streams_active() { - var sm = CreateMachine(new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(1) }.ToEngineOptions()); + var sm = CreateMachine(new TurboClientOptions { Http3 = new Http3Options { IdleTimeout = TimeSpan.FromMilliseconds(1) } }); sm.EncodeRequest(CreateGetRequest()); await Task.Delay(20, TestContext.Current.CancellationToken); @@ -404,7 +403,7 @@ public void OnConnectionRestored_should_clear_reconnect_state() [Fact(Timeout = 5000)] public void OnReconnectAttemptFailed_should_signal_after_max_attempts() { - var options = new Http3Options { MaxReconnectAttempts = 2 }.ToEngineOptions(); + var options = new TurboClientOptions { Http3 = new Http3Options { MaxReconnectAttempts = 2 } }; var sm = CreateMachine(options); sm.OnConnectionLost(); // attempt 1 @@ -419,7 +418,7 @@ public void OnReconnectAttemptFailed_should_signal_after_max_attempts() [Fact(Timeout = 5000)] public void OnReconnectAttemptFailed_should_allow_retry_before_max() { - var options = new Http3Options { MaxReconnectAttempts = 3 }.ToEngineOptions(); + var options = new TurboClientOptions { Http3 = new Http3Options { MaxReconnectAttempts = 3 } }; var sm = CreateMachine(options); sm.OnConnectionLost(); // attempt 1 @@ -560,10 +559,10 @@ public void EncodeRequest_should_tag_outbound_frames_with_stream_id() sm.EncodeRequest(CreateGetRequest()); - // All request frames should be tagged as Http3NetworkBuffer with stream ID 0 + // All request frames should be tagged as RoutedNetworkBuffer with stream ID 0 var tagged = _ops.Outbound - .OfType() - .Where(t => t.StreamType == Http3StreamType.Request) + .OfType() + .Where(t => t.StreamTypeValue is null) .ToList(); Assert.NotEmpty(tagged); Assert.All(tagged, t => Assert.Equal(0L, t.StreamId)); diff --git a/src/TurboHTTP.Tests/Http3/Connection/Http3StreamRoutingSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/Http3StreamRoutingSpec.cs index b22f85b91..6a05b2675 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/Http3StreamRoutingSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/Http3StreamRoutingSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; using TurboHTTP.Protocol.Http3.Qpack; using TurboHTTP.Tests.Shared; @@ -13,7 +13,7 @@ public sealed class Http3StreamRoutingSpec private StateMachine CreateMachine(FakeOps? ops = null) { return new StateMachine( - new Http3Options().ToEngineOptions(), + new TurboClientOptions(), ops ?? _ops); } diff --git a/src/TurboHTTP.Tests/Http3/Connection/QuicConnectionMigrationSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/QuicConnectionMigrationSpec.cs index 7687e643c..de75debe2 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/QuicConnectionMigrationSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/QuicConnectionMigrationSpec.cs @@ -1,10 +1,9 @@ using System.Net; using Akka.Actor; using Akka.Event; -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; namespace TurboHTTP.Tests.Http3.Connection; @@ -28,18 +27,18 @@ public void Http3Options_should_accept_AllowConnectionMigration_false() [Fact(Timeout = 5000)] [Trait("RFC", "RFC9000-9")] - public void Http3EngineOptions_should_default_AllowConnectionMigration_to_true() + public void TurboClientOptions_should_default_Http3_AllowConnectionMigration_to_true() { - var options = new Http3Options().ToEngineOptions(); - Assert.True(options.AllowConnectionMigration); + var options = new TurboClientOptions(); + Assert.True(options.Http3.AllowConnectionMigration); } [Fact(Timeout = 5000)] [Trait("RFC", "RFC9000-9")] - public void Http3EngineOptions_should_accept_AllowConnectionMigration_false() + public void TurboClientOptions_should_accept_Http3_AllowConnectionMigration_false() { - var options = new Http3Options { AllowConnectionMigration = false }.ToEngineOptions(); - Assert.False(options.AllowConnectionMigration); + var options = new TurboClientOptions { Http3 = new Http3Options { AllowConnectionMigration = false } }; + Assert.False(options.Http3.AllowConnectionMigration); } [Fact(Timeout = 5000)] @@ -64,8 +63,7 @@ public void Migration_allowed_should_continue_transparently_when_address_changes { // Arrange var ops = new StubTransportOperations(); - var sm = new QuicTransportStateMachine(ops, Nobody.Instance, Nobody.Instance, - new TurboClientOptions(), allowConnectionMigration: true); + var sm = new QuicTransportStateMachine(ops, Nobody.Instance, Nobody.Instance, allowConnectionMigration: true); var oldEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.10"), 12345); var newEndPoint = new IPEndPoint(IPAddress.Parse("10.0.0.5"), 54321); @@ -83,8 +81,7 @@ public void Migration_disallowed_should_trigger_reconnect_when_address_changes() { // Arrange var ops = new StubTransportOperations(); - var sm = new QuicTransportStateMachine(ops, Nobody.Instance, Nobody.Instance, - new TurboClientOptions(), allowConnectionMigration: false); + var sm = new QuicTransportStateMachine(ops, Nobody.Instance, Nobody.Instance, allowConnectionMigration: false); var oldEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.10"), 12345); var newEndPoint = new IPEndPoint(IPAddress.Parse("10.0.0.5"), 54321); diff --git a/src/TurboHTTP.Tests/Http3/Connection/QuicMultiStreamSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/QuicMultiStreamSpec.cs index 4903144e2..d206edf9f 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/QuicMultiStreamSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/QuicMultiStreamSpec.cs @@ -1,5 +1,7 @@ using System.Net; -using TurboHTTP.Transport.Connection; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; namespace TurboHTTP.Tests.Http3.Connection; diff --git a/src/TurboHTTP.Tests/Http3/Connection/SniTlsEnforcementSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/SniTlsEnforcementSpec.cs index 167330139..9ce7653e3 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/SniTlsEnforcementSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/SniTlsEnforcementSpec.cs @@ -1,7 +1,8 @@ using System.Net; using System.Net.Security; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.Tests.Http3.Connection; diff --git a/src/TurboHTTP.Tests/Http3/Connection/TransportSelectionSpec.cs b/src/TurboHTTP.Tests/Http3/Connection/TransportSelectionSpec.cs index 6890aaa8a..a0b0b74e5 100644 --- a/src/TurboHTTP.Tests/Http3/Connection/TransportSelectionSpec.cs +++ b/src/TurboHTTP.Tests/Http3/Connection/TransportSelectionSpec.cs @@ -1,6 +1,8 @@ using System.Net; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.Tests.Http3.Connection; diff --git a/src/TurboHTTP.Tests/Internal/NetworkBufferPoolSpec.cs b/src/TurboHTTP.Tests/Internal/NetworkBufferPoolSpec.cs index 46d3abc51..b5fae7df4 100644 --- a/src/TurboHTTP.Tests/Internal/NetworkBufferPoolSpec.cs +++ b/src/TurboHTTP.Tests/Internal/NetworkBufferPoolSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Internal; diff --git a/src/TurboHTTP.Tests/Internal/RequestEndpointSpec.cs b/src/TurboHTTP.Tests/Internal/RequestEndpointSpec.cs index 06ebe24e7..53d537b84 100644 --- a/src/TurboHTTP.Tests/Internal/RequestEndpointSpec.cs +++ b/src/TurboHTTP.Tests/Internal/RequestEndpointSpec.cs @@ -1,5 +1,5 @@ using System.Net; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests.Internal; diff --git a/src/TurboHTTP.Tests/ModuleInit.cs b/src/TurboHTTP.Tests/ModuleInit.cs index d75ba9149..3891b6e56 100644 --- a/src/TurboHTTP.Tests/ModuleInit.cs +++ b/src/TurboHTTP.Tests/ModuleInit.cs @@ -1,5 +1,5 @@ using System.Runtime.CompilerServices; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Tests; diff --git a/src/TurboHTTP.Tests/Transport/OptionsFactorySpec.cs b/src/TurboHTTP.Tests/OptionsFactorySpec.cs similarity index 99% rename from src/TurboHTTP.Tests/Transport/OptionsFactorySpec.cs rename to src/TurboHTTP.Tests/OptionsFactorySpec.cs index 9d89a5129..d2512d68a 100644 --- a/src/TurboHTTP.Tests/Transport/OptionsFactorySpec.cs +++ b/src/TurboHTTP.Tests/OptionsFactorySpec.cs @@ -1,9 +1,11 @@ using System.Net; using System.Net.Security; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; -namespace TurboHTTP.Tests.Transport; +namespace TurboHTTP.Tests; public sealed class OptionsFactorySpec { diff --git a/src/TurboHTTP.Tests/Security/TlsOptionsSpec.cs b/src/TurboHTTP.Tests/Security/TlsOptionsSpec.cs index 3223ed14e..1803867b7 100644 --- a/src/TurboHTTP.Tests/Security/TlsOptionsSpec.cs +++ b/src/TurboHTTP.Tests/Security/TlsOptionsSpec.cs @@ -2,9 +2,11 @@ using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; using TurboHTTP.Internal; using TurboHTTP.Protocol.Semantics; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.Tests.Security; diff --git a/src/TurboHTTP.Tests/Security/TlsSecuritySpec.cs b/src/TurboHTTP.Tests/Security/TlsSecuritySpec.cs index 59e9f2746..3741eb4bb 100644 --- a/src/TurboHTTP.Tests/Security/TlsSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Security/TlsSecuritySpec.cs @@ -1,8 +1,9 @@ using System.Net; using System.Net.Security; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; using TurboHTTP.Internal; using TurboHTTP.Protocol.Semantics; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.Tests.Security; diff --git a/src/TurboHTTP.Tests/Security/UriSecuritySpec.cs b/src/TurboHTTP.Tests/Security/UriSecuritySpec.cs index cf3f803b5..03ceef303 100644 --- a/src/TurboHTTP.Tests/Security/UriSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Security/UriSecuritySpec.cs @@ -1,6 +1,5 @@ using System.Net; using System.Text; -using TurboHTTP.Protocol.Http2.Hpack; using TurboHTTP.Protocol.Semantics; using Encoder = TurboHTTP.Protocol.Http11.Encoder; diff --git a/src/TurboHTTP.Tests/Semantics/CertificateValidationSpec.cs b/src/TurboHTTP.Tests/Semantics/CertificateValidationSpec.cs index 5c7dea380..bf0449ac9 100644 --- a/src/TurboHTTP.Tests/Semantics/CertificateValidationSpec.cs +++ b/src/TurboHTTP.Tests/Semantics/CertificateValidationSpec.cs @@ -1,7 +1,8 @@ using System.Net; using System.Net.Security; +using Servus.Akka.IO; +using Servus.Akka.IO.Tcp; using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; namespace TurboHTTP.Tests.Semantics; diff --git a/src/TurboHTTP.Tests/Semantics/ProtocolCoreBuilderLimitsSpec.cs b/src/TurboHTTP.Tests/Semantics/ProtocolCoreBuilderLimitsSpec.cs index ae9af6d3f..87b0d4b17 100644 --- a/src/TurboHTTP.Tests/Semantics/ProtocolCoreBuilderLimitsSpec.cs +++ b/src/TurboHTTP.Tests/Semantics/ProtocolCoreBuilderLimitsSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; namespace TurboHTTP.Tests.Semantics; diff --git a/src/TurboHTTP.Tests/Streams/ConnectionShapeSpec.cs b/src/TurboHTTP.Tests/Streams/ConnectionShapeSpec.cs index 4017b7cbc..214282b8c 100644 --- a/src/TurboHTTP.Tests/Streams/ConnectionShapeSpec.cs +++ b/src/TurboHTTP.Tests/Streams/ConnectionShapeSpec.cs @@ -1,5 +1,5 @@ using Akka.Streams; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; namespace TurboHTTP.Tests.Streams; @@ -93,7 +93,7 @@ public void ConnectionShape_should_copy_from_ports() var newInlets = new[] { inServer.CarbonCopy(), inApp.CarbonCopy() }; var newOutlets = new[] { outResponse.CarbonCopy(), outNetwork.CarbonCopy() }; - var copiedShape = shape.CopyFromPorts([..newInlets], [..newOutlets]); + var copiedShape = shape.CopyFromPorts([.. newInlets], [.. newOutlets]); Assert.IsType(copiedShape); var result = (ConnectionShape)copiedShape; @@ -178,7 +178,7 @@ public void ConnectionShape_copy_from_ports_should_preserve_port_types() var newInlets = new[] { inServer.CarbonCopy(), inApp.CarbonCopy() }; var newOutlets = new[] { outResponse.CarbonCopy(), outNetwork.CarbonCopy() }; - var copiedShape = shape.CopyFromPorts([..newInlets], [..newOutlets]); + var copiedShape = shape.CopyFromPorts([.. newInlets], [.. newOutlets]); var result = (ConnectionShape)copiedShape; Assert.IsType>(result.InServer); diff --git a/src/TurboHTTP.Tests/Streams/EngineSpec.cs b/src/TurboHTTP.Tests/Streams/EngineSpec.cs index 65fa42f40..dd9381425 100644 --- a/src/TurboHTTP.Tests/Streams/EngineSpec.cs +++ b/src/TurboHTTP.Tests/Streams/EngineSpec.cs @@ -1,6 +1,6 @@ using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; namespace TurboHTTP.Tests.Streams; diff --git a/src/TurboHTTP.slnx b/src/TurboHTTP.slnx index d23f780d2..60c9d2c0f 100644 --- a/src/TurboHTTP.slnx +++ b/src/TurboHTTP.slnx @@ -15,7 +15,10 @@ + + + diff --git a/src/TurboHTTP/Diagnostics/LoggerTraceListener.cs b/src/TurboHTTP/Diagnostics/LoggerTraceListener.cs index 356c6a94b..d5a7a7ac7 100644 --- a/src/TurboHTTP/Diagnostics/LoggerTraceListener.cs +++ b/src/TurboHTTP/Diagnostics/LoggerTraceListener.cs @@ -53,16 +53,11 @@ private static Dictionary CreateLoggers(ILoggerFact { return new Dictionary { - [TurboTraceCategory.Connection] = loggerFactory.CreateLogger("TurboHTTP.Trace.Connection"), [TurboTraceCategory.Protocol] = loggerFactory.CreateLogger("TurboHTTP.Trace.Protocol"), [TurboTraceCategory.Request] = loggerFactory.CreateLogger("TurboHTTP.Trace.Request"), - [TurboTraceCategory.Response] = loggerFactory.CreateLogger("TurboHTTP.Trace.Response"), [TurboTraceCategory.Cache] = loggerFactory.CreateLogger("TurboHTTP.Trace.Cache"), [TurboTraceCategory.Redirect] = loggerFactory.CreateLogger("TurboHTTP.Trace.Redirect"), [TurboTraceCategory.Retry] = loggerFactory.CreateLogger("TurboHTTP.Trace.Retry"), - [TurboTraceCategory.Pool] = loggerFactory.CreateLogger("TurboHTTP.Trace.Pool"), - [TurboTraceCategory.Transport] = loggerFactory.CreateLogger("TurboHTTP.Trace.Transport"), - [TurboTraceCategory.Stream] = loggerFactory.CreateLogger("TurboHTTP.Trace.Stream"), }; } } \ No newline at end of file diff --git a/src/TurboHTTP/Diagnostics/TurboHttpDiagnosticSource.cs b/src/TurboHTTP/Diagnostics/TurboHttpDiagnosticSource.cs deleted file mode 100644 index 81378c256..000000000 --- a/src/TurboHTTP/Diagnostics/TurboHttpDiagnosticSource.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Diagnostics; - -namespace TurboHTTP.Diagnostics; - -/// -/// Provides events for TurboHTTP, following the same -/// event patterns as System.Net.Http's DiagnosticListener. -/// -/// Subscribers filter via DiagnosticListener.AllListeners.Subscribe(...) -/// with listener name "TurboHTTP". -/// -/// -/// Events emitted: -/// -/// TurboHTTP.HttpRequestOut.Start — request about to be sent -/// TurboHTTP.HttpRequestOut.Stop — request completed (success or failure) -/// TurboHTTP.Exception — exception during request processing -/// -/// -/// -internal static class TurboHttpDiagnosticSource -{ - /// - /// The name. Subscribe with - /// DiagnosticListener.AllListeners.Subscribe(observer) - /// and filter for this name. - /// - public const string ListenerName = "TurboHTTP"; - - private static readonly DiagnosticListener Listener = new(ListenerName); - - /// - /// Returns true when at least one subscriber is listening for request events. - /// Use this to guard payload construction. - /// - public static bool IsEnabled => Listener.IsEnabled("TurboHTTP.HttpRequestOut"); - - /// - /// Emits the TurboHTTP.HttpRequestOut.Start event. - /// - public static void OnRequestStart(HttpRequestMessage request) - { - if (Listener.IsEnabled("TurboHTTP.HttpRequestOut.Start")) - { - Listener.Write("TurboHTTP.HttpRequestOut.Start", new { Request = request }); - } - } - - /// - /// Emits the TurboHTTP.HttpRequestOut.Stop event. - /// - /// The original request message. - /// The response, or null if the request failed. - /// The final of the request. - public static void OnRequestStop( - HttpRequestMessage request, - HttpResponseMessage? response, - TaskStatus taskStatus) - { - if (Listener.IsEnabled("TurboHTTP.HttpRequestOut")) - { - Listener.Write("TurboHTTP.HttpRequestOut.Stop", new - { - Request = request, - Response = response, - RequestTaskStatus = taskStatus, - }); - } - } - - /// - /// Emits the TurboHTTP.Exception event. - /// - public static void OnException(HttpRequestMessage request, Exception exception) - { - if (Listener.IsEnabled("TurboHTTP.Exception")) - { - Listener.Write("TurboHTTP.Exception", new - { - Request = request, - Exception = exception, - }); - } - } -} diff --git a/src/TurboHTTP/Diagnostics/TurboHttpEventSource.cs b/src/TurboHTTP/Diagnostics/TurboHttpEventSource.cs deleted file mode 100644 index bf0708f39..000000000 --- a/src/TurboHTTP/Diagnostics/TurboHttpEventSource.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Diagnostics.Tracing; - -namespace TurboHTTP.Diagnostics; - -/// -/// High-performance for TurboHTTP. -/// Enables zero-alloc structured logging for production diagnostics via ETW (Windows), -/// EventPipe, or dotnet-trace. -/// -/// Enable with: dotnet-trace collect -p {pid} --providers TurboHTTP -/// -/// -[EventSource(Name = "TurboHTTP")] -internal sealed class TurboHttpEventSource : EventSource -{ - /// - /// Singleton instance. - /// - public static readonly TurboHttpEventSource Instance = new(); - - private TurboHttpEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) - { - } - - [Event(1, Level = EventLevel.Informational, Opcode = EventOpcode.Start, - Keywords = Keywords.Request, Message = "Request started: {0} {1}")] - public void RequestStart(string method, string url) - { - if (IsEnabled(EventLevel.Informational, Keywords.Request)) - { - WriteEvent(1, method, url); - } - } - - [Event(2, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, - Keywords = Keywords.Request, Message = "Request completed: {0} {1} {2}ms")] - public void RequestStop(string method, int statusCode, double durationMs) - { - if (IsEnabled(EventLevel.Informational, Keywords.Request)) - { - WriteEvent(2, method, statusCode, durationMs); - } - } - - [Event(3, Level = EventLevel.Error, Keywords = Keywords.Request, - Message = "Request failed: {0} {1} — {2}")] - public void RequestFailed(string method, string url, string exceptionType) - { - if (IsEnabled(EventLevel.Error, Keywords.Request)) - { - WriteEvent(3, method, url, exceptionType); - } - } - - [Event(10, Level = EventLevel.Informational, Opcode = EventOpcode.Start, - Keywords = Keywords.Connection, Message = "Connection opening: {0}:{1}")] - public void ConnectionStart(string host, int port) - { - if (IsEnabled(EventLevel.Informational, Keywords.Connection)) - { - WriteEvent(10, host, port); - } - } - - [Event(11, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, - Keywords = Keywords.Connection, Message = "Connection closed: {0}:{1} ({2}ms)")] - public void ConnectionStop(string host, int port, double durationMs) - { - if (IsEnabled(EventLevel.Informational, Keywords.Connection)) - { - WriteEvent(11, host, port, durationMs); - } - } - - [Event(20, Level = EventLevel.Informational, Opcode = EventOpcode.Start, - Keywords = Keywords.Dns, Message = "DNS lookup: {0}")] - public void DnsLookupStart(string hostname) - { - if (IsEnabled(EventLevel.Informational, Keywords.Dns)) - { - WriteEvent(20, hostname); - } - } - - [Event(21, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, - Keywords = Keywords.Dns, Message = "DNS lookup completed: {0} ({1}ms)")] - public void DnsLookupStop(string hostname, double durationMs) - { - if (IsEnabled(EventLevel.Informational, Keywords.Dns)) - { - WriteEvent(21, hostname, durationMs); - } - } - - [Event(30, Level = EventLevel.Informational, Opcode = EventOpcode.Start, - Keywords = Keywords.Tls, Message = "TLS handshake: {0}")] - public void TlsHandshakeStart(string host) - { - if (IsEnabled(EventLevel.Informational, Keywords.Tls)) - { - WriteEvent(30, host); - } - } - - [Event(31, Level = EventLevel.Informational, Opcode = EventOpcode.Stop, - Keywords = Keywords.Tls, Message = "TLS handshake completed: {0} ({1}ms)")] - public void TlsHandshakeStop(string host, double durationMs) - { - if (IsEnabled(EventLevel.Informational, Keywords.Tls)) - { - WriteEvent(31, host, durationMs); - } - } - - [Event(40, Level = EventLevel.Informational, Keywords = Keywords.Redirect, - Message = "Redirect: {0} → {1}")] - public void Redirect(int statusCode, string targetUrl) - { - if (IsEnabled(EventLevel.Informational, Keywords.Redirect)) - { - WriteEvent(40, statusCode, targetUrl); - } - } - - [Event(50, Level = EventLevel.Warning, Keywords = Keywords.Retry, - Message = "Retry attempt {0}")] - public void RetryAttempt(int attemptNumber) - { - if (IsEnabled(EventLevel.Warning, Keywords.Retry)) - { - WriteEvent(50, attemptNumber); - } - } - - [Event(60, Level = EventLevel.Informational, Keywords = Keywords.Cache, - Message = "Cache hit: {0}")] - public void CacheHit(string url) - { - if (IsEnabled(EventLevel.Informational, Keywords.Cache)) - { - WriteEvent(60, url); - } - } - - [Event(61, Level = EventLevel.Informational, Keywords = Keywords.Cache, - Message = "Cache miss: {0}")] - public void CacheMiss(string url) - { - if (IsEnabled(EventLevel.Informational, Keywords.Cache)) - { - WriteEvent(61, url); - } - } - - /// - /// ETW keyword categories for filtering event streams. - /// - public static class Keywords - { - public const EventKeywords Request = (EventKeywords)0x01; - public const EventKeywords Connection = (EventKeywords)0x02; - public const EventKeywords Dns = (EventKeywords)0x04; - public const EventKeywords Tls = (EventKeywords)0x08; - public const EventKeywords Redirect = (EventKeywords)0x10; - public const EventKeywords Retry = (EventKeywords)0x20; - public const EventKeywords Cache = (EventKeywords)0x40; - } -} diff --git a/src/TurboHTTP/Diagnostics/TurboHttpInstrumentation.cs b/src/TurboHTTP/Diagnostics/TurboHttpInstrumentation.cs index a373789a3..6ec6cebda 100644 --- a/src/TurboHTTP/Diagnostics/TurboHttpInstrumentation.cs +++ b/src/TurboHTTP/Diagnostics/TurboHttpInstrumentation.cs @@ -136,175 +136,6 @@ public static string FormatProtocolVersion(Version version) return activity; } - /// - /// Starts a "TurboHTTP.Connect" activity for a connection attempt. - /// - public static Activity? StartConnect(Uri uri) - { - if (!Source.HasListeners()) - { - return null; - } - - var activity = Source.StartActivity( - $"{SourceName}.Connect", - ActivityKind.Client); - - if (activity is null) - { - return null; - } - - activity.SetTag("server.address", uri.Host); - activity.SetTag("server.port", uri.Port); - activity.SetTag("url.scheme", uri.Scheme); - - return activity; - } - - /// - /// Starts a "TurboHTTP.DnsLookup" activity for a DNS resolution. - /// - public static Activity? StartDnsLookup(string hostname) - { - if (!Source.HasListeners()) - { - return null; - } - - var activity = Source.StartActivity( - $"{SourceName}.DnsLookup", - ActivityKind.Client); - - if (activity is null) - { - return null; - } - - activity.SetTag("dns.question.name", hostname); - - return activity; - } - - /// - /// Starts a "TurboHTTP.SocketConnect" activity for a TCP socket connection. - /// - /// The peer IP address (e.g., "93.184.216.34"). - /// The peer port number. - /// The transport protocol: "tcp", "udp", or "unix". - /// The network type: "ipv4" or "ipv6". Null for non-IP transports. - public static Activity? StartSocketConnect(string address, int port, - string transport = "tcp", string? networkType = null) - { - if (!Source.HasListeners()) - { - return null; - } - - var activity = Source.StartActivity( - $"{SourceName}.SocketConnect", - ActivityKind.Client); - - if (activity is null) - { - return null; - } - - activity.SetTag("network.peer.address", address); - activity.SetTag("network.peer.port", port); - activity.SetTag("network.transport", transport); - if (networkType is not null) - { - activity.SetTag("network.type", networkType); - } - - return activity; - } - - /// - /// Starts a "TurboHTTP.TlsHandshake" activity for a TLS negotiation. - /// After the handshake completes, call to record - /// tls.protocol.name and tls.protocol.version. - /// - public static Activity? StartTlsHandshake(string host) - { - if (!Source.HasListeners()) - { - return null; - } - - var activity = Source.StartActivity( - $"{SourceName}.TlsHandshake", - ActivityKind.Client); - - if (activity is null) - { - return null; - } - - activity.SetTag("server.address", host); - - return activity; - } - - /// - /// Enriches a TLS handshake activity with protocol information after negotiation completes. - /// - /// The TLS handshake activity. - /// The protocol name, e.g. "tls" or "ssl". - /// The protocol version, e.g. "1.2" or "1.3". - public static void SetTlsInfo(Activity activity, string protocolName, string protocolVersion) - { - activity.SetTag("tls.protocol.name", protocolName); - activity.SetTag("tls.protocol.version", protocolVersion); - } - - /// - /// Enriches a DNS lookup activity with resolved addresses after resolution completes. - /// - /// The DNS lookup activity. - /// The resolved IP addresses. - public static void SetDnsAnswers(Activity activity, string[] answers) - { - activity.SetTag("dns.answers", answers); - } - - /// - /// Enriches a Connect activity with the resolved peer address after connection is established. - /// - /// The connect activity. - /// The peer IP address, e.g. "93.184.216.34". - public static void SetNetworkPeerAddress(Activity activity, string address) - { - activity.SetTag("network.peer.address", address); - } - - /// - /// Starts a "TurboHTTP.WaitForConnection" activity for time spent waiting - /// for an available connection from the pool. - /// - public static Activity? StartWaitForConnection(string address, int port) - { - if (!Source.HasListeners()) - { - return null; - } - - var activity = Source.StartActivity( - $"{SourceName}.WaitForConnection", - ActivityKind.Client); - - if (activity is null) - { - return null; - } - - activity.SetTag("server.address", address); - activity.SetTag("server.port", port); - - return activity; - } - /// /// Starts a "TurboHTTP.Redirect" activity for a redirect hop. /// diff --git a/src/TurboHTTP/Diagnostics/TurboHttpMetrics.cs b/src/TurboHTTP/Diagnostics/TurboHttpMetrics.cs index a6090947f..500009c22 100644 --- a/src/TurboHTTP/Diagnostics/TurboHttpMetrics.cs +++ b/src/TurboHTTP/Diagnostics/TurboHttpMetrics.cs @@ -85,27 +85,6 @@ internal static class TurboHttpMetrics unit: "{redirect}", description: "Number of HTTP redirect hops"); - /// - /// Connection lifetime in seconds. - /// - public static Histogram ConnectionDuration { get; } = - Meter.CreateHistogram( - "http.client.connection.duration", - unit: "s", - description: "Duration of HTTP connections in seconds"); - - /// - /// Number of open HTTP connections. - /// Tags: http.connection.state ("active" or "idle"), - /// server.address, server.port. - /// Matches .NET HttpClient's http.client.open_connections instrument. - /// - public static UpDownCounter OpenConnections { get; } = - Meter.CreateUpDownCounter( - "http.client.open_connections", - unit: "{connection}", - description: "Number of currently open HTTP connections"); - /// /// Currently active (in-flight) HTTP requests. /// Tags: http.request.method, server.address, server.port, url.scheme. @@ -116,26 +95,6 @@ internal static class TurboHttpMetrics unit: "{request}", description: "Number of currently active HTTP requests"); - /// - /// Time HTTP requests spend waiting for an available connection from the pool. - /// Tags: http.request.method, server.address, server.port, url.scheme. - /// - public static Histogram RequestTimeInQueue { get; } = - Meter.CreateHistogram( - "http.client.request.time_in_queue", - unit: "s", - description: "Time HTTP requests spend waiting for a connection"); - - /// - /// Duration of DNS lookups. - /// Tags: dns.question.name, error.type (if failed). - /// - public static Histogram DnsLookupDuration { get; } = - Meter.CreateHistogram( - "dns.lookup.duration", - unit: "s", - description: "Duration of DNS lookups"); - /// /// Pipeline stall events detected by PipelineHealthMonitorStage. /// Tags: stage, direction. diff --git a/src/TurboHTTP/Diagnostics/TurboTrace.cs b/src/TurboHTTP/Diagnostics/TurboTrace.cs index c9344d19e..abd7f39bd 100644 --- a/src/TurboHTTP/Diagnostics/TurboTrace.cs +++ b/src/TurboHTTP/Diagnostics/TurboTrace.cs @@ -53,87 +53,6 @@ private sealed record TraceConfig( TurboTraceCategory EnabledCategories, TurboTraceLevel MinimumLevel); - /// Trace category for connection lifecycle events. - public static class Connection - { - private const TurboTraceCategory Category = TurboTraceCategory.Connection; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Trace(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Trace(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Debug(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Info(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Info(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message)); - } - - public static void Warning(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Error(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - } - /// Trace category for protocol-level events (HTTP/1.1, HTTP/2, HTTP/3). public static class Protocol { @@ -296,87 +215,6 @@ public static void Error(object source, string message, params object?[] args) } } - /// Trace category for response processing events. - public static class Response - { - private const TurboTraceCategory Category = TurboTraceCategory.Response; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Trace(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Trace(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Debug(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Info(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Info(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message)); - } - - public static void Warning(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Error(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - } - /// Trace category for cache events. public static class Cache { @@ -627,251 +465,4 @@ public static void Error(object source, string message, params object?[] args) source.GetHashCode(), message, args)); } } - - /// Trace category for connection pool events. - public static class Pool - { - private const TurboTraceCategory Category = TurboTraceCategory.Pool; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Trace(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Trace(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Debug(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Info(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Info(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Warning(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, args)); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Error(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - } - - /// Trace category for transport-level events (TCP, QUIC). - public static class Transport - { - private const TurboTraceCategory Category = TurboTraceCategory.Transport; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Trace(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Trace(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Debug(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Info(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Info(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, 0, null, null, null)); - } - - public static void Warning(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Error(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - } - - /// Trace category for stream multiplexing events (HTTP/2, HTTP/3). - public static class Stream - { - private const TurboTraceCategory Category = TurboTraceCategory.Stream; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Trace(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Trace(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Trace)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Trace, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Debug(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Debug(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Debug)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Debug, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Info(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Info(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Info)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Info, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Warning(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message)); - } - - public static void Warning(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Warning)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Warning, Category, - source.GetType().Name, source.GetHashCode(), message, args)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Error(object source, string message) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message)); - } - - public static void Error(object source, string message, params object?[] args) - { - if (!ShouldTrace(Category, TurboTraceLevel.Error)) return; - WriteEvent(new TraceEvent(Stopwatch.GetTimestamp(), TurboTraceLevel.Error, Category, source.GetType().Name, - source.GetHashCode(), message, args)); - } - } } \ No newline at end of file diff --git a/src/TurboHTTP/Diagnostics/TurboTraceCategory.cs b/src/TurboHTTP/Diagnostics/TurboTraceCategory.cs index 90164026c..8504bd57c 100644 --- a/src/TurboHTTP/Diagnostics/TurboTraceCategory.cs +++ b/src/TurboHTTP/Diagnostics/TurboTraceCategory.cs @@ -8,15 +8,10 @@ namespace TurboHTTP.Diagnostics; public enum TurboTraceCategory : ushort { None = 0, - Connection = 1, Protocol = 2, Request = 4, - Response = 8, Cache = 16, Redirect = 32, Retry = 64, - Pool = 128, - Transport = 256, - Stream = 512, - All = 1023, + All = 118, } diff --git a/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs b/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs index 1641a86e8..28c213505 100644 --- a/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs +++ b/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; +using Servus.Akka.Diagnostics; namespace TurboHTTP.Diagnostics; @@ -53,14 +54,4 @@ public static IServiceCollection AddTurboTracing( services.AddSingleton(listener); return services; } - - public static MeterProviderBuilder AddTurboHttpMetrics(this MeterProviderBuilder builder) - { - return builder.AddMeter(TurboHttpMetrics.MeterName); - } - - public static TracerProviderBuilder AddTurboHttpTracing(this TracerProviderBuilder builder) - { - return builder.AddSource(TurboHttpInstrumentation.SourceName); - } } \ No newline at end of file diff --git a/src/TurboHTTP/Internal/OptionsExtensions.cs b/src/TurboHTTP/Internal/OptionsExtensions.cs deleted file mode 100644 index 24c79888e..000000000 --- a/src/TurboHTTP/Internal/OptionsExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using TurboHTTP.Streams; - -namespace TurboHTTP.Internal; - -internal static class OptionsExtensions -{ - public static Http1EngineOptions ToEngineOptions(this Http1Options options) - { - return new Http1EngineOptions( - options.MaxPipelineDepth, - options.MaxConnectionsPerServer, - options.MaxReconnectAttempts, - options.MaxBatchWeight, - options.MaxResponseHeadersLength, - options.MaxResponseDrainSize, - options.ResponseDrainTimeout); - } - - public static Http2EngineOptions ToEngineOptions(this Http2Options options) - { - return new Http2EngineOptions( - options.MaxConnectionsPerServer, - options.MaxConcurrentStreams, - options.InitialConnectionWindowSize, - options.InitialStreamWindowSize, - options.MaxFrameSize, - options.HeaderTableSize, - options.MaxReconnectAttempts, - options.MaxBatchWeight, - options.KeepAlivePingDelay, - options.KeepAlivePingTimeout, - options.KeepAlivePingPolicy); - } - - public static Http3EngineOptions ToEngineOptions(this Http3Options options) - { - return new Http3EngineOptions( - options.MaxFieldSectionSize, - options.QpackMaxTableCapacity, - options.QpackBlockedStreams, - options.IdleTimeout, - options.MaxReconnectAttempts, - options.AllowServerPush, - options.AllowEarlyData, - options.AllowConnectionMigration); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/OptionsFactory.cs b/src/TurboHTTP/Internal/OptionsFactory.cs similarity index 96% rename from src/TurboHTTP/Transport/Connection/OptionsFactory.cs rename to src/TurboHTTP/Internal/OptionsFactory.cs index 16a03239c..d1c76de4e 100644 --- a/src/TurboHTTP/Transport/Connection/OptionsFactory.cs +++ b/src/TurboHTTP/Internal/OptionsFactory.cs @@ -1,7 +1,9 @@ using System.Net.Security; -using TurboHTTP.Internal; +using Servus.Akka.IO; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; -namespace TurboHTTP.Transport.Connection; +namespace TurboHTTP.Internal; internal static class OptionsFactory { diff --git a/src/TurboHTTP/Internal/PooledBodyContent.cs b/src/TurboHTTP/Internal/PooledBodyContent.cs index 270e4064b..ab8d779f7 100644 --- a/src/TurboHTTP/Internal/PooledBodyContent.cs +++ b/src/TurboHTTP/Internal/PooledBodyContent.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Net; +using Servus.Akka.IO; namespace TurboHTTP.Internal; diff --git a/src/TurboHTTP/Protocol/Http10/StateMachine.cs b/src/TurboHTTP/Protocol/Http10/StateMachine.cs index 24754fa1a..d844596d2 100644 --- a/src/TurboHTTP/Protocol/Http10/StateMachine.cs +++ b/src/TurboHTTP/Protocol/Http10/StateMachine.cs @@ -1,3 +1,4 @@ +using Servus.Akka.IO; using TurboHTTP.Internal; using TurboHTTP.Protocol.Http11; using TurboHTTP.Streams.Stages; @@ -15,10 +16,9 @@ internal sealed class StateMachine private readonly Decoder _decoder; private readonly int _minBufferSize; private readonly int _maxBufferSize; - private readonly int _maxReconnectAttempts; - private readonly int _maxResponseDrainSize; - private readonly TimeSpan _responseDrainTimeout; + private readonly TurboClientOptions _options; + private ITransportOptions? _transportOptions; private HttpRequestMessage? _inFlightRequest; private bool _closed; private HttpRequestMessage? _reconnectBufferedRequest; @@ -37,27 +37,24 @@ internal sealed class StateMachine /// Number of requests currently buffered or in-flight (used for discard logging). public int PendingRequestCount => _reconnecting ? _reconnectBufferedRequest is not null ? 1 : 0 - : _inFlightRequest is not null ? 1 : 0; + : _inFlightRequest is not null + ? 1 + : 0; /// The current connection endpoint. public RequestEndpoint Endpoint { get; private set; } public StateMachine( IStageOperations ops, - int maxReconnectAttempts = 3, + TurboClientOptions options, int minBufferSize = 4 * 1024, - int maxBufferSize = 256 * 1024, - int maxResponseHeadersLength = 64, - int maxResponseDrainSize = 1024 * 1024, - TimeSpan? responseDrainTimeout = null) + int maxBufferSize = 256 * 1024) { _ops = ops; - _decoder = new Decoder(maxTotalHeaderSize: maxResponseHeadersLength * 1024); - _maxReconnectAttempts = maxReconnectAttempts; + _options = options; + _decoder = new Decoder(maxTotalHeaderSize: options.Http1.MaxResponseHeadersLength * 1024); _minBufferSize = minBufferSize; _maxBufferSize = maxBufferSize; - _maxResponseDrainSize = maxResponseDrainSize; - _responseDrainTimeout = responseDrainTimeout ?? TimeSpan.FromSeconds(2); } /// @@ -73,6 +70,12 @@ public void EncodeRequest(HttpRequestMessage request) if (Endpoint == default && endpoint != default) { Endpoint = endpoint; + _transportOptions = OptionsFactory.Build(endpoint, _options); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + Options = _transportOptions + }); } // Emit StreamAcquireItem before request data @@ -189,7 +192,7 @@ public void MarkClosed() } /// - /// Buffers the in-flight request and emits a ReconnectItem to trigger a new TCP connection. + /// Buffers the in-flight request and emits a ConnectItem (reconnect) to trigger a new TCP connection. /// Call when a CloseSignalItem arrives with an in-flight request and we are not yet reconnecting. /// public void StartReconnect() @@ -198,7 +201,12 @@ public void StartReconnect() _inFlightRequest = null; _reconnecting = true; _reconnectAttempts = 1; - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + IsReconnect = true, + Options = _transportOptions! + }); } /// @@ -220,18 +228,23 @@ public void OnConnectionRestored() /// /// Called when a CloseSignalItem arrives while already reconnecting (reconnect attempt failed). - /// Increments the attempt counter; emits a new ReconnectItem or calls OnReconnectFailed. + /// Increments the attempt counter; emits a new ConnectItem (reconnect) or calls OnReconnectFailed. /// public void OnReconnectAttemptFailed() { - if (_reconnectAttempts >= _maxReconnectAttempts) + if (_reconnectAttempts >= _options.Http1.MaxReconnectAttempts) { _ops.OnReconnectFailed(); return; } _reconnectAttempts++; - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + IsReconnect = true, + Options = _transportOptions! + }); } /// @@ -280,7 +293,7 @@ private void CompleteResponse(HttpResponseMessage response) // HTTP/1.0 default is Connection: close (RFC 1945) var endpoint = RequestEndpoint.FromRequest(response.RequestMessage!); var decision = ConnectionReuseEvaluator.Evaluate(response, response.Version); - var item = new ConnectionReuseItem(decision) { Key = endpoint }; + var item = new ConnectionReuseItem(decision.CanReuse) { Key = endpoint }; _ops.OnResponse(response); _ops.OnOutbound(item); } diff --git a/src/TurboHTTP/Protocol/Http11/StateMachine.cs b/src/TurboHTTP/Protocol/Http11/StateMachine.cs index b57769958..26a8269f4 100644 --- a/src/TurboHTTP/Protocol/Http11/StateMachine.cs +++ b/src/TurboHTTP/Protocol/Http11/StateMachine.cs @@ -1,3 +1,4 @@ +using Servus.Akka.IO; using TurboHTTP.Internal; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages; @@ -15,15 +16,13 @@ internal sealed class StateMachine private readonly Decoder _decoder; private readonly int _minBufferSize; private readonly int _maxBufferSize; - private readonly int _maxPipelineDepth; - private readonly int _maxReconnectAttempts; - private readonly int _maxResponseDrainSize; - private readonly TimeSpan _responseDrainTimeout; + private readonly TurboClientOptions _options; private readonly Queue _inFlightQueue = new(); - private int _effectivePipelineDepth; private Queue? _reconnectBufferedQueue; + private int _effectivePipelineDepth; private int _reconnectAttempts; + private ITransportOptions? _transportOptions; /// /// Holds a response whose body is delimited by connection close (no Content-Length, @@ -59,23 +58,16 @@ internal sealed class StateMachine public StateMachine( IStageOperations ops, - int maxPipelineDepth = 8, - int maxReconnectAttempts = 3, + TurboClientOptions options, int minBufferSize = 4 * 1024, - int maxBufferSize = 256 * 1024, - int maxResponseHeadersLength = 64, - int maxResponseDrainSize = 1024 * 1024, - TimeSpan? responseDrainTimeout = null) + int maxBufferSize = 256 * 1024) { _ops = ops; - _decoder = new Decoder(maxTotalHeaderSize: maxResponseHeadersLength * 1024); - _maxPipelineDepth = maxPipelineDepth; - _effectivePipelineDepth = maxPipelineDepth; - _maxReconnectAttempts = maxReconnectAttempts; + _options = options; + _decoder = new Decoder(maxTotalHeaderSize: options.Http1.MaxResponseHeadersLength * 1024); _minBufferSize = minBufferSize; _maxBufferSize = maxBufferSize; - _maxResponseDrainSize = maxResponseDrainSize; - _responseDrainTimeout = responseDrainTimeout ?? TimeSpan.FromSeconds(2); + _effectivePipelineDepth = options.Http1.MaxPipelineDepth; } /// @@ -91,6 +83,12 @@ public void EncodeRequest(HttpRequestMessage request) if (Endpoint == default && endpoint != default) { Endpoint = endpoint; + _transportOptions = OptionsFactory.Build(Endpoint, _options); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + Options = _transportOptions + }); } // Emit StreamAcquireItem before request data @@ -258,7 +256,7 @@ public void HandleOrphanedRequests() } /// - /// Buffers all in-flight requests and emits a ReconnectItem to trigger a new TCP connection. + /// Buffers all in-flight requests and emits a ConnectItem (reconnect) to trigger a new TCP connection. /// Call when a CloseSignalItem arrives with in-flight requests and we are not yet reconnecting. /// public void StartReconnect() @@ -268,7 +266,12 @@ public void StartReconnect() IsReconnecting = true; _reconnectAttempts = 1; _decoder.Reset(); - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + IsReconnect = true, + Options = _transportOptions! + }); } /// @@ -293,18 +296,23 @@ public void OnConnectionRestored() /// /// Called when a CloseSignalItem arrives while already reconnecting (reconnect attempt failed). - /// Increments the attempt counter; emits a new ReconnectItem or calls OnReconnectFailed. + /// Increments the attempt counter; emits a new ConnectItem (reconnect) or calls OnReconnectFailed. /// public void OnReconnectAttemptFailed() { - if (_reconnectAttempts >= _maxReconnectAttempts) + if (_reconnectAttempts >= _options.Http1.MaxReconnectAttempts) { _ops.OnReconnectFailed(); return; } _reconnectAttempts++; - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + IsReconnect = true, + Options = _transportOptions! + }); } /// @@ -412,7 +420,7 @@ private void CompleteResponse(HttpResponseMessage response) var decision = ConnectionReuseEvaluator.Evaluate(response, response.Version); _ops.OnResponse(response); - var item = new ConnectionReuseItem(decision) { Key = endpoint }; + var item = new ConnectionReuseItem(decision.CanReuse) { Key = endpoint }; _ops.OnOutbound(item); } @@ -449,6 +457,4 @@ private static bool HasConnectionClose(HttpResponseMessage response) { return response.Headers.ConnectionClose == true; } - - } \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Http11/StatusLineDecoder.cs b/src/TurboHTTP/Protocol/Http11/StatusLineDecoder.cs index bceda1de3..2f44b536a 100644 --- a/src/TurboHTTP/Protocol/Http11/StatusLineDecoder.cs +++ b/src/TurboHTTP/Protocol/Http11/StatusLineDecoder.cs @@ -81,19 +81,19 @@ internal static bool TryParse(ReadOnlySpan line, out int statusCode, out s private static string GetOrCreateReasonPhrase(ReadOnlySpan span) => span.Length switch { - 2 => span.SequenceEqual("OK"u8) ? "OK" : Encoding.ASCII.GetString(span), - 5 => span.SequenceEqual("Found"u8) ? "Found" : Encoding.ASCII.GetString(span), - 7 => span.SequenceEqual("Created"u8) ? "Created" : Encoding.ASCII.GetString(span), - 8 => span.SequenceEqual("Accepted"u8) ? "Accepted" : Encoding.ASCII.GetString(span), - 9 => span.SequenceEqual("Not Found"u8) ? "Not Found" : - span.SequenceEqual("Forbidden"u8) ? "Forbidden" : Encoding.ASCII.GetString(span), - 10 => span.SequenceEqual("No Content"u8) ? "No Content" : Encoding.ASCII.GetString(span), - 11 => span.SequenceEqual("Bad Request"u8) ? "Bad Request" : Encoding.ASCII.GetString(span), - 12 => span.SequenceEqual("Unauthorized"u8) ? "Unauthorized" : - span.SequenceEqual("Not Modified"u8) ? "Not Modified" : Encoding.ASCII.GetString(span), - 15 => span.SequenceEqual("Partial Content"u8) ? "Partial Content" : Encoding.ASCII.GetString(span), - 17 => span.SequenceEqual("Moved Permanently"u8) ? "Moved Permanently" : Encoding.ASCII.GetString(span), + 2 => span.SequenceEqual("OK"u8) ? "OK" : Encoding.ASCII.GetString(span), + 5 => span.SequenceEqual("Found"u8) ? "Found" : Encoding.ASCII.GetString(span), + 7 => span.SequenceEqual("Created"u8) ? "Created" : Encoding.ASCII.GetString(span), + 8 => span.SequenceEqual("Accepted"u8) ? "Accepted" : Encoding.ASCII.GetString(span), + 9 => span.SequenceEqual("Not Found"u8) ? "Not Found" : + span.SequenceEqual("Forbidden"u8) ? "Forbidden" : Encoding.ASCII.GetString(span), + 10 => span.SequenceEqual("No Content"u8) ? "No Content" : Encoding.ASCII.GetString(span), + 11 => span.SequenceEqual("Bad Request"u8) ? "Bad Request" : Encoding.ASCII.GetString(span), + 12 => span.SequenceEqual("Unauthorized"u8) ? "Unauthorized" : + span.SequenceEqual("Not Modified"u8) ? "Not Modified" : Encoding.ASCII.GetString(span), + 15 => span.SequenceEqual("Partial Content"u8) ? "Partial Content" : Encoding.ASCII.GetString(span), + 17 => span.SequenceEqual("Moved Permanently"u8) ? "Moved Permanently" : Encoding.ASCII.GetString(span), 21 => span.SequenceEqual("Internal Server Error"u8) ? "Internal Server Error" : Encoding.ASCII.GetString(span), - _ => Encoding.ASCII.GetString(span), + _ => Encoding.ASCII.GetString(span), }; } diff --git a/src/TurboHTTP/Protocol/Http2/FrameDecoder.cs b/src/TurboHTTP/Protocol/Http2/FrameDecoder.cs index d45aba81a..edae7c1ee 100644 --- a/src/TurboHTTP/Protocol/Http2/FrameDecoder.cs +++ b/src/TurboHTTP/Protocol/Http2/FrameDecoder.cs @@ -1,5 +1,5 @@ using System.Buffers.Binary; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Protocol.Http2; diff --git a/src/TurboHTTP/Protocol/Http2/RequestEncoder.cs b/src/TurboHTTP/Protocol/Http2/RequestEncoder.cs index cd8188588..3b25597fa 100644 --- a/src/TurboHTTP/Protocol/Http2/RequestEncoder.cs +++ b/src/TurboHTTP/Protocol/Http2/RequestEncoder.cs @@ -242,9 +242,9 @@ internal static void ValidatePseudoHeaders(List headers) hasAuthority = true; break; default: - { - throw new Http2Exception($"RFC 9113 §8.3.1: Unknown request pseudo-header '{name}'"); - } + { + throw new Http2Exception($"RFC 9113 §8.3.1: Unknown request pseudo-header '{name}'"); + } } } else @@ -335,17 +335,17 @@ public void ApplyServerSettings(IEnumerable<(SettingsParameter Key, uint Value)> _hpack.AcknowledgeTableSizeChange((int)val); break; case SettingsParameter.InitialWindowSize: - { - // RFC 9113 §6.9.2: Apply delta to all existing stream send windows - var delta = val - _initialSendStreamWindow; - _initialSendStreamWindow = val; - foreach (var streamId in _streamSendWindows.Keys) { - _streamSendWindows[streamId] += delta; - } + // RFC 9113 §6.9.2: Apply delta to all existing stream send windows + var delta = val - _initialSendStreamWindow; + _initialSendStreamWindow = val; + foreach (var streamId in _streamSendWindows.Keys) + { + _streamSendWindows[streamId] += delta; + } - break; - } + break; + } } } } diff --git a/src/TurboHTTP/Protocol/Http2/StateMachine.cs b/src/TurboHTTP/Protocol/Http2/StateMachine.cs index 8b716888b..b14ccec60 100644 --- a/src/TurboHTTP/Protocol/Http2/StateMachine.cs +++ b/src/TurboHTTP/Protocol/Http2/StateMachine.cs @@ -1,8 +1,7 @@ +using Servus.Akka.IO; using TurboHTTP.Internal; -using TurboHTTP.Protocol.Http11; using TurboHTTP.Protocol.Http2.Hpack; using TurboHTTP.Protocol.Semantics; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages; namespace TurboHTTP.Protocol.Http2; @@ -16,7 +15,7 @@ namespace TurboHTTP.Protocol.Http2; internal sealed class StateMachine { private const int MaxStatePoolCapacity = 1000; - private readonly Http2EngineOptions _options; + private readonly TurboClientOptions _options; private readonly IStageOperations _ops; @@ -29,6 +28,7 @@ internal sealed class StateMachine private readonly Dictionary _streams = new(); private readonly Stack _statePool; + private ITransportOptions? _transportOptions; private int _statePoolCapacity; private bool _prefaceSent; @@ -52,14 +52,14 @@ internal sealed class StateMachine /// The current connection endpoint. public RequestEndpoint Endpoint { get; private set; } - public StateMachine(Http2EngineOptions options, IStageOperations ops) + public StateMachine(TurboClientOptions options, IStageOperations ops) { _options = options; _ops = ops; - _tracker = new StreamTracker(1, options.InitialConcurrentStreams); - _connection = new ConnectionState(options.InitialConnectionWindowSize, - options.InitialStreamWindowSize); - _requestEncoder = new RequestEncoder(maxFrameSize: options.MaxFrameSize); + _tracker = new StreamTracker(1, options.Http2.MaxConcurrentStreams); + _connection = new ConnectionState(options.Http2.InitialConnectionWindowSize, + options.Http2.InitialStreamWindowSize); + _requestEncoder = new RequestEncoder(maxFrameSize: options.Http2.MaxFrameSize); _statePoolCapacity = Math.Min( _tracker.MaxConcurrentStreams > 0 ? _tracker.MaxConcurrentStreams : 100, MaxStatePoolCapacity); @@ -72,16 +72,16 @@ public StateMachine(Http2EngineOptions options, IStageOperations ops) /// public NetworkBuffer? TryBuildPreface() { - if (_options.InitialConnectionWindowSize <= 0 || _prefaceSent) + if (_options.Http2.InitialConnectionWindowSize <= 0 || _prefaceSent) { return null; } _prefaceSent = true; var (prefaceOwner, prefaceLength) = PrefaceBuilder.Build( - _options.InitialConnectionWindowSize, - _options.HeaderTableSize, - _options.MaxFrameSize); + _options.Http2.InitialConnectionWindowSize, + _options.Http2.HeaderTableSize, + _options.Http2.MaxFrameSize); var prefaceBuf = NetworkBuffer.Rent(prefaceLength); prefaceOwner.Memory.Span[..prefaceLength].CopyTo(prefaceBuf.FullMemory.Span); prefaceOwner.Dispose(); @@ -212,6 +212,12 @@ public bool EncodeRequest(HttpRequestMessage request) if (Endpoint == default && endpoint != default) { Endpoint = endpoint; + _transportOptions = OptionsFactory.Build(Endpoint, _options); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + Options = _transportOptions + }); } _correlationMap.TryAdd(streamId, request); @@ -286,9 +292,8 @@ private bool HandleInboundData(DataFrame frame) if (result.IsConnectionViolation) { _ops.OnWarning("RFC 9113 §6.9 — connection flow control window exceeded. Triggering reconnect."); - var item = new ConnectionReuseItem( - ConnectionReuseDecision.Close("RFC 9113 §6.9: connection window exceeded")) - { Key = Endpoint }; + var item = new ConnectionReuseItem(false) + { Key = Endpoint }; _ops.OnOutbound(item); return false; } @@ -297,8 +302,8 @@ private bool HandleInboundData(DataFrame frame) { _ops.OnWarning( $"RFC 9113 §6.9 — stream {frame.StreamId} flow control window exceeded. Triggering reconnect."); - var item = new ConnectionReuseItem(ConnectionReuseDecision.Close("RFC 9113 §6.9: stream window exceeded")) - { Key = Endpoint }; + var item = new ConnectionReuseItem(false) + { Key = Endpoint }; _ops.OnOutbound(item); return false; } @@ -389,7 +394,7 @@ private void ReturnState(StreamState state) /// /// Called when the TCP connection is lost (GOAWAY or abrupt close) with in-flight requests. /// Classifies streams by LastStreamId and idempotency, buffers safe-to-replay requests, - /// resets all connection state, and emits a ReconnectItem. + /// resets all connection state, and emits a ConnectItem (reconnect). /// public void OnConnectionLost(int lastStreamId) { @@ -399,7 +404,7 @@ public void OnConnectionLost(int lastStreamId) IsReconnecting = true; _reconnectAttempts = 1; - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem { Key = Endpoint, IsReconnect = true, Options = _transportOptions! }); } private void ClassifyStreamsForReplay(int lastStreamId) @@ -444,7 +449,7 @@ private void ReleaseAllStreamState() private void ResetConnectionState() { _tracker.Reset(); - _connection.Reset(_options.InitialConnectionWindowSize, _options.InitialStreamWindowSize); + _connection.Reset(_options.Http2.InitialConnectionWindowSize, _options.Http2.InitialStreamWindowSize); _requestEncoder.ResetHpack(); _responseDecoder.ResetHpack(); _prefaceSent = false; @@ -477,18 +482,18 @@ public void OnConnectionRestored() /// /// Called when a CloseSignalItem arrives while already reconnecting (reconnect attempt failed). - /// Increments the attempt counter; emits a new ReconnectItem or calls OnReconnectFailed. + /// Increments the attempt counter; emits a new ConnectItem (reconnect) or calls OnReconnectFailed. /// public void OnReconnectAttemptFailed() { - if (_reconnectAttempts >= _options.MaxReconnectAttempts) + if (_reconnectAttempts >= _options.Http2.MaxReconnectAttempts) { _ops.OnReconnectFailed(); return; } _reconnectAttempts++; - _ops.OnOutbound(new ReconnectItem { Key = Endpoint }); + _ops.OnOutbound(new ConnectItem { Key = Endpoint, IsReconnect = true, Options = _transportOptions! }); } private static bool IsIdempotentMethod(HttpMethod method) diff --git a/src/TurboHTTP/Protocol/Http3/Qpack/QpackTableSync.cs b/src/TurboHTTP/Protocol/Http3/Qpack/QpackTableSync.cs index 5674a488b..3d449c1eb 100644 --- a/src/TurboHTTP/Protocol/Http3/Qpack/QpackTableSync.cs +++ b/src/TurboHTTP/Protocol/Http3/Qpack/QpackTableSync.cs @@ -235,14 +235,14 @@ private void ApplyEncoderInstruction(EncoderInstruction instruction) switch (instruction.Type) { case EncoderInstructionType.InsertWithNameReference: - { - var name = instruction.IsStatic - ? QpackStaticTable.Entries[instruction.NameIndex].Name - : Decoder.DynamicTable.GetEntry( - Decoder.DynamicTable.InsertCount - 1 - instruction.NameIndex)!.Value.Name; - Decoder.DynamicTable.Insert(name, instruction.ValueString); - break; - } + { + var name = instruction.IsStatic + ? QpackStaticTable.Entries[instruction.NameIndex].Name + : Decoder.DynamicTable.GetEntry( + Decoder.DynamicTable.InsertCount - 1 - instruction.NameIndex)!.Value.Name; + Decoder.DynamicTable.Insert(name, instruction.ValueString); + break; + } case EncoderInstructionType.InsertWithLiteralName: Decoder.DynamicTable.Insert(instruction.NameString, instruction.ValueString); diff --git a/src/TurboHTTP/Protocol/Http3/QpackStreamHandler.cs b/src/TurboHTTP/Protocol/Http3/QpackStreamHandler.cs index 1d84bf02b..157a04359 100644 --- a/src/TurboHTTP/Protocol/Http3/QpackStreamHandler.cs +++ b/src/TurboHTTP/Protocol/Http3/QpackStreamHandler.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3.Qpack; using TurboHTTP.Streams.Stages; @@ -80,7 +80,7 @@ public void FlushDecoderInstructions(RequestEndpoint endpoint) { var sectionAck = _responseDecoder.DecoderInstructions; - var buf = Http3NetworkBuffer.Rent(1 + sectionAck.Length + 16); + var buf = RoutedNetworkBuffer.Rent(1 + sectionAck.Length + 16); var dest = buf.FullMemory.Span; var offset = 0; @@ -109,7 +109,7 @@ public void FlushDecoderInstructions(RequestEndpoint endpoint) _decoderPrefaceSent = true; buf.Length = offset; buf.Key = endpoint; - buf.StreamType = Http3StreamType.QpackDecoder; + buf.StreamTypeValue = 0x03; _ops.OnOutbound(buf); } @@ -143,11 +143,11 @@ public void FlushEncoderInstructions(RequestEndpoint endpoint) totalLength = instructions.Length; } - var buf = Http3NetworkBuffer.Rent(totalLength); + var buf = RoutedNetworkBuffer.Rent(totalLength); owner.Memory.Span[..totalLength].CopyTo(buf.FullMemory.Span); buf.Length = totalLength; buf.Key = endpoint; - buf.StreamType = Http3StreamType.QpackEncoder; + buf.StreamTypeValue = 0x02; _ops.OnOutbound(buf); } diff --git a/src/TurboHTTP/Protocol/Http3/Settings.cs b/src/TurboHTTP/Protocol/Http3/Settings.cs index 91be5f901..460e06b52 100644 --- a/src/TurboHTTP/Protocol/Http3/Settings.cs +++ b/src/TurboHTTP/Protocol/Http3/Settings.cs @@ -100,14 +100,14 @@ public static Settings Deserialize(ReadOnlySpan payload) { if (!QuicVarInt.TryDecode(payload, out var identifier, out var consumed)) { - throw new Http3Exception(Http3ErrorCode.SettingsError,"Incomplete setting identifier in SETTINGS payload."); + throw new Http3Exception(Http3ErrorCode.SettingsError, "Incomplete setting identifier in SETTINGS payload."); } payload = payload[consumed..]; if (!QuicVarInt.TryDecode(payload, out var value, out consumed)) { - throw new Http3Exception(Http3ErrorCode.SettingsError,"Incomplete setting value in SETTINGS payload."); + throw new Http3Exception(Http3ErrorCode.SettingsError, "Incomplete setting value in SETTINGS payload."); } payload = payload[consumed..]; diff --git a/src/TurboHTTP/Protocol/Http3/StateMachine.cs b/src/TurboHTTP/Protocol/Http3/StateMachine.cs index 9943144c4..a5fd2d83c 100644 --- a/src/TurboHTTP/Protocol/Http3/StateMachine.cs +++ b/src/TurboHTTP/Protocol/Http3/StateMachine.cs @@ -1,8 +1,8 @@ using System.Buffers; using System.Security.Cryptography.X509Certificates; +using Servus.Akka.IO; using TurboHTTP.Internal; using TurboHTTP.Protocol.Http3.Qpack; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages; namespace TurboHTTP.Protocol.Http3; @@ -34,8 +34,9 @@ internal sealed class StateMachine : IDisposable HttpMethod.Delete, ]; - private readonly Http3EngineOptions _options; + private readonly TurboClientOptions _options; private readonly IStageOperations _ops; + private ITransportOptions? _transportOptions; private readonly RequestEncoder _requestEncoder; private readonly ResponseDecoder _responseDecoder; @@ -76,7 +77,7 @@ internal sealed class StateMachine : IDisposable /// The QPACK table synchronization coordinator. internal QpackTableSync TableSync { get; } - public StateMachine(Http3EngineOptions options, IStageOperations ops) + public StateMachine(TurboClientOptions options, IStageOperations ops) { _options = options; _ops = ops; @@ -86,7 +87,7 @@ public StateMachine(Http3EngineOptions options, IStageOperations ops) TableSync = new QpackTableSync( encoderMaxCapacity: 0, decoderMaxCapacity: 4096, - maxBlockedStreams: options.QpackBlockedStreams); + maxBlockedStreams: options.Http3.QpackBlockedStreams); _requestEncoder = new RequestEncoder(TableSync); _responseDecoder = new ResponseDecoder(TableSync); _qpackHandler = new QpackStreamHandler(ops, _requestEncoder, _responseDecoder, TableSync); @@ -97,11 +98,11 @@ public StateMachine(Http3EngineOptions options, IStageOperations ops) }; Tracker = new StreamTracker(); - var idleTimeout = options.IdleTimeout == TimeSpan.Zero + var idleTimeout = options.Http3.IdleTimeout == TimeSpan.Zero ? DefaultIdleTimeout - : options.IdleTimeout; + : options.Http3.IdleTimeout; - Connection = new ConnectionState(idleTimeout, options.AllowServerPush ? 100 : 0); + Connection = new ConnectionState(idleTimeout, options.Http3.AllowServerPush ? 100 : 0); } /// @@ -126,7 +127,7 @@ public StateMachine(Http3EngineOptions options, IStageOperations ops) var totalSize = streamTypeSize + frameSize; Http3MaxPushIdFrame? maxPushIdFrame = null; - if (_options.AllowServerPush) + if (_options.Http3.AllowServerPush) { maxPushIdFrame = new Http3MaxPushIdFrame(99); totalSize += maxPushIdFrame.SerializedSize; @@ -141,11 +142,11 @@ public StateMachine(Http3EngineOptions options, IStageOperations ops) maxPushIdFrame?.WriteTo(ref span); - var buf = Http3NetworkBuffer.Rent(totalSize); + var buf = RoutedNetworkBuffer.Rent(totalSize); owner.Memory.Span[..totalSize].CopyTo(buf.FullMemory.Span); buf.Length = totalSize; buf.Key = Endpoint; - buf.StreamType = Http3StreamType.Control; + buf.StreamTypeValue = 0x00; return buf; } @@ -153,9 +154,9 @@ public StateMachine(Http3EngineOptions options, IStageOperations ops) /// /// Decodes a NetworkBuffer into HTTP/3 frames using a per-stream decoder. /// - public IReadOnlyList DecodeServerData(NetworkBuffer buffer, long streamId) + public IReadOnlyList DecodeServerData(NetworkBuffer buffer, long? streamId) { - return _streamManager.DecodeServerData(buffer, streamId); + return _streamManager.DecodeServerData(buffer, streamId!.Value); } /// @@ -351,7 +352,7 @@ public void OnConnectionRestored() /// public bool OnReconnectAttemptFailed() { - if (_reconnectAttempts >= _options.MaxReconnectAttempts) + if (_reconnectAttempts >= _options.Http3.MaxReconnectAttempts) { _ops.OnReconnectFailed(); return false; @@ -393,6 +394,12 @@ private bool EncodeAndEmit(HttpRequestMessage request) if (Endpoint == default && endpoint != default) { Endpoint = endpoint; + _transportOptions = OptionsFactory.Build(Endpoint, _options); + _ops.OnOutbound(new ConnectItem + { + Key = Endpoint, + Options = _transportOptions + }); } var streamId = Tracker.AllocateStreamId(); @@ -417,7 +424,7 @@ private IReadOnlyList EncodeToFrames(HttpRequestMessage request) OriginValidator.Validate(request.RequestUri!, request.Method == HttpMethod.Connect); var frames = _requestEncoder.Encode(request); - if (_options.AllowEarlyData && IdempotentMethods.Contains(request.Method)) + if (_options.Http3.AllowEarlyData && IdempotentMethods.Contains(request.Method)) { foreach (var f in frames) { @@ -460,7 +467,7 @@ private void ReplayBufferedFrames() private void EmitSerializedFrame(Http3Frame frame, long streamId = -1) { - var buf = Http3NetworkBuffer.Rent(frame.SerializedSize); + var buf = RoutedNetworkBuffer.Rent(frame.SerializedSize); var span = buf.FullMemory.Span; frame.WriteTo(ref span); buf.Length = frame.SerializedSize; @@ -468,7 +475,7 @@ private void EmitSerializedFrame(Http3Frame frame, long streamId = -1) if (streamId >= 0) { - buf.StreamType = Http3StreamType.Request; + buf.StreamTypeValue = null; buf.StreamId = streamId; } @@ -512,7 +519,7 @@ private void HandleGoAway(Http3GoAwayFrame goAway) private Http3PushPromiseFrame? HandlePushPromise(Http3PushPromiseFrame pushPromise) { - if (!_options.AllowServerPush) + if (!_options.Http3.AllowServerPush) { var cancelFrame = new Http3CancelPushFrame(pushPromise.PushId); EmitSerializedFrame(cancelFrame); diff --git a/src/TurboHTTP/Protocol/Http3/StreamManager.cs b/src/TurboHTTP/Protocol/Http3/StreamManager.cs index 662d94307..ae55c7039 100644 --- a/src/TurboHTTP/Protocol/Http3/StreamManager.cs +++ b/src/TurboHTTP/Protocol/Http3/StreamManager.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3.Qpack; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Streams.Stages; diff --git a/src/TurboHTTP/Protocol/WellKnownHeaders.cs b/src/TurboHTTP/Protocol/WellKnownHeaders.cs index b21f1b7f3..ee9a3f67c 100644 --- a/src/TurboHTTP/Protocol/WellKnownHeaders.cs +++ b/src/TurboHTTP/Protocol/WellKnownHeaders.cs @@ -130,58 +130,58 @@ public static class Names public static string GetOrCreateHeaderName(ReadOnlySpan name) => name.Length switch { - 2 => name.SequenceEqual("TE"u8) ? "TE" : System.Text.Encoding.ASCII.GetString(name), - 3 => name.SequenceEqual("Age"u8) ? "Age" : - name.SequenceEqual("Via"u8) ? "Via" : System.Text.Encoding.ASCII.GetString(name), - 4 => name.SequenceEqual("Date"u8) ? "Date" : - name.SequenceEqual("ETag"u8) ? "ETag" : - name.SequenceEqual("Vary"u8) ? "Vary" : - name.SequenceEqual("From"u8) ? "From" : - name.SequenceEqual("Host"u8) ? "Host" : - name.SequenceEqual("Link"u8) ? "Link" : System.Text.Encoding.ASCII.GetString(name), - 5 => name.SequenceEqual("Allow"u8) ? "Allow" : - name.SequenceEqual("Retry"u8) ? "Retry" : System.Text.Encoding.ASCII.GetString(name), - 6 => name.SequenceEqual("Accept"u8) ? "Accept" : - name.SequenceEqual("Cookie"u8) ? "Cookie" : - name.SequenceEqual("Expect"u8) ? "Expect" : - name.SequenceEqual("Pragma"u8) ? "Pragma" : - name.SequenceEqual("Server"u8) ? "Server" : System.Text.Encoding.ASCII.GetString(name), - 7 => name.SequenceEqual("Alt-Svc"u8) ? "Alt-Svc" : - name.SequenceEqual("Expires"u8) ? "Expires" : - name.SequenceEqual("Referer"u8) ? "Referer" : - name.SequenceEqual("Trailer"u8) ? "Trailer" : - name.SequenceEqual("Upgrade"u8) ? "Upgrade" : - name.SequenceEqual("Warning"u8) ? "Warning" : System.Text.Encoding.ASCII.GetString(name), - 8 => name.SequenceEqual("If-Match"u8) ? "If-Match" : - name.SequenceEqual("If-Range"u8) ? "If-Range" : - name.SequenceEqual("Location"u8) ? "Location" : System.Text.Encoding.ASCII.GetString(name), - 10 => name.SequenceEqual("Connection"u8) ? "Connection" : - name.SequenceEqual("Set-Cookie"u8) ? "Set-Cookie" : - name.SequenceEqual("User-Agent"u8) ? "User-Agent" : System.Text.Encoding.ASCII.GetString(name), - 11 => name.SequenceEqual("Retry-After"u8) ? "Retry-After" : - name.SequenceEqual("Set-Cookie2"u8) ? "Set-Cookie2" : System.Text.Encoding.ASCII.GetString(name), - 12 => name.SequenceEqual("Content-Type"u8) ? "Content-Type" : - name.SequenceEqual("Last-Modified"u8) ? "Last-Modified" : - name.SequenceEqual("Max-Forwards"u8) ? "Max-Forwards" : System.Text.Encoding.ASCII.GetString(name), - 13 => name.SequenceEqual("Authorization"u8) ? "Authorization" : - name.SequenceEqual("Cache-Control"u8) ? "Cache-Control" : - name.SequenceEqual("Content-Range"u8) ? "Content-Range" : System.Text.Encoding.ASCII.GetString(name), - 14 => name.SequenceEqual("Accept-Charset"u8) ? "Accept-Charset" : - name.SequenceEqual("Accept-Ranges"u8) ? "Accept-Ranges" : - name.SequenceEqual("Content-Length"u8) ? "Content-Length" : System.Text.Encoding.ASCII.GetString(name), - 15 => name.SequenceEqual("Accept-Encoding"u8) ? "Accept-Encoding" : - name.SequenceEqual("Accept-Language"u8) ? "Accept-Language" : System.Text.Encoding.ASCII.GetString(name), - 16 => name.SequenceEqual("Content-Encoding"u8) ? "Content-Encoding" : - name.SequenceEqual("Content-Language"u8) ? "Content-Language" : - name.SequenceEqual("Content-Location"u8) ? "Content-Location" : - name.SequenceEqual("WWW-Authenticate"u8) ? "WWW-Authenticate" : System.Text.Encoding.ASCII.GetString(name), - 17 => name.SequenceEqual("If-Modified-Since"u8) ? "If-Modified-Since" : - name.SequenceEqual("Transfer-Encoding"u8) ? "Transfer-Encoding" : System.Text.Encoding.ASCII.GetString(name), - 18 => name.SequenceEqual("Proxy-Authenticate"u8) ? "Proxy-Authenticate" : System.Text.Encoding.ASCII.GetString(name), - 19 => name.SequenceEqual("If-Unmodified-Since"u8) ? "If-Unmodified-Since" : - name.SequenceEqual("Proxy-Authorization"u8) ? "Proxy-Authorization" : System.Text.Encoding.ASCII.GetString(name), + 2 => name.SequenceEqual("TE"u8) ? "TE" : System.Text.Encoding.ASCII.GetString(name), + 3 => name.SequenceEqual("Age"u8) ? "Age" : + name.SequenceEqual("Via"u8) ? "Via" : System.Text.Encoding.ASCII.GetString(name), + 4 => name.SequenceEqual("Date"u8) ? "Date" : + name.SequenceEqual("ETag"u8) ? "ETag" : + name.SequenceEqual("Vary"u8) ? "Vary" : + name.SequenceEqual("From"u8) ? "From" : + name.SequenceEqual("Host"u8) ? "Host" : + name.SequenceEqual("Link"u8) ? "Link" : System.Text.Encoding.ASCII.GetString(name), + 5 => name.SequenceEqual("Allow"u8) ? "Allow" : + name.SequenceEqual("Retry"u8) ? "Retry" : System.Text.Encoding.ASCII.GetString(name), + 6 => name.SequenceEqual("Accept"u8) ? "Accept" : + name.SequenceEqual("Cookie"u8) ? "Cookie" : + name.SequenceEqual("Expect"u8) ? "Expect" : + name.SequenceEqual("Pragma"u8) ? "Pragma" : + name.SequenceEqual("Server"u8) ? "Server" : System.Text.Encoding.ASCII.GetString(name), + 7 => name.SequenceEqual("Alt-Svc"u8) ? "Alt-Svc" : + name.SequenceEqual("Expires"u8) ? "Expires" : + name.SequenceEqual("Referer"u8) ? "Referer" : + name.SequenceEqual("Trailer"u8) ? "Trailer" : + name.SequenceEqual("Upgrade"u8) ? "Upgrade" : + name.SequenceEqual("Warning"u8) ? "Warning" : System.Text.Encoding.ASCII.GetString(name), + 8 => name.SequenceEqual("If-Match"u8) ? "If-Match" : + name.SequenceEqual("If-Range"u8) ? "If-Range" : + name.SequenceEqual("Location"u8) ? "Location" : System.Text.Encoding.ASCII.GetString(name), + 10 => name.SequenceEqual("Connection"u8) ? "Connection" : + name.SequenceEqual("Set-Cookie"u8) ? "Set-Cookie" : + name.SequenceEqual("User-Agent"u8) ? "User-Agent" : System.Text.Encoding.ASCII.GetString(name), + 11 => name.SequenceEqual("Retry-After"u8) ? "Retry-After" : + name.SequenceEqual("Set-Cookie2"u8) ? "Set-Cookie2" : System.Text.Encoding.ASCII.GetString(name), + 12 => name.SequenceEqual("Content-Type"u8) ? "Content-Type" : + name.SequenceEqual("Last-Modified"u8) ? "Last-Modified" : + name.SequenceEqual("Max-Forwards"u8) ? "Max-Forwards" : System.Text.Encoding.ASCII.GetString(name), + 13 => name.SequenceEqual("Authorization"u8) ? "Authorization" : + name.SequenceEqual("Cache-Control"u8) ? "Cache-Control" : + name.SequenceEqual("Content-Range"u8) ? "Content-Range" : System.Text.Encoding.ASCII.GetString(name), + 14 => name.SequenceEqual("Accept-Charset"u8) ? "Accept-Charset" : + name.SequenceEqual("Accept-Ranges"u8) ? "Accept-Ranges" : + name.SequenceEqual("Content-Length"u8) ? "Content-Length" : System.Text.Encoding.ASCII.GetString(name), + 15 => name.SequenceEqual("Accept-Encoding"u8) ? "Accept-Encoding" : + name.SequenceEqual("Accept-Language"u8) ? "Accept-Language" : System.Text.Encoding.ASCII.GetString(name), + 16 => name.SequenceEqual("Content-Encoding"u8) ? "Content-Encoding" : + name.SequenceEqual("Content-Language"u8) ? "Content-Language" : + name.SequenceEqual("Content-Location"u8) ? "Content-Location" : + name.SequenceEqual("WWW-Authenticate"u8) ? "WWW-Authenticate" : System.Text.Encoding.ASCII.GetString(name), + 17 => name.SequenceEqual("If-Modified-Since"u8) ? "If-Modified-Since" : + name.SequenceEqual("Transfer-Encoding"u8) ? "Transfer-Encoding" : System.Text.Encoding.ASCII.GetString(name), + 18 => name.SequenceEqual("Proxy-Authenticate"u8) ? "Proxy-Authenticate" : System.Text.Encoding.ASCII.GetString(name), + 19 => name.SequenceEqual("If-Unmodified-Since"u8) ? "If-Unmodified-Since" : + name.SequenceEqual("Proxy-Authorization"u8) ? "Proxy-Authorization" : System.Text.Encoding.ASCII.GetString(name), 25 => name.SequenceEqual("Strict-Transport-Security"u8) ? "Strict-Transport-Security" : System.Text.Encoding.ASCII.GetString(name), - _ => System.Text.Encoding.ASCII.GetString(name), + _ => System.Text.Encoding.ASCII.GetString(name), }; /// @@ -198,25 +198,25 @@ public static string GetOrCreateHeaderName(ReadOnlySpan name) public static string GetOrCreateHeaderValue(ReadOnlySpan value) => value.Length switch { - 1 => value.SequenceEqual("0"u8) ? "0" : - value.SequenceEqual("1"u8) ? "1" : System.Text.Encoding.ASCII.GetString(value), - 2 => value.SequenceEqual("br"u8) ? "br" : System.Text.Encoding.ASCII.GetString(value), - 4 => value.SequenceEqual("gzip"u8) ? "gzip" : - value.SequenceEqual("none"u8) ? "none" : System.Text.Encoding.ASCII.GetString(value), - 5 => value.SequenceEqual("close"u8) ? "close" : - value.SequenceEqual("bytes"u8) ? "bytes" : System.Text.Encoding.ASCII.GetString(value), - 6 => value.SequenceEqual("public"u8) ? "public" : System.Text.Encoding.ASCII.GetString(value), - 7 => value.SequenceEqual("chunked"u8) ? "chunked" : - value.SequenceEqual("deflate"u8) ? "deflate" : - value.SequenceEqual("private"u8) ? "private" : - value.SequenceEqual("trailer"u8) ? "trailer" : System.Text.Encoding.ASCII.GetString(value), - 8 => value.SequenceEqual("compress"u8) ? "compress" : - value.SequenceEqual("identity"u8) ? "identity" : - value.SequenceEqual("no-cache"u8) ? "no-cache" : - value.SequenceEqual("no-store"u8) ? "no-store" : - value.SequenceEqual("trailers"u8) ? "trailers" : System.Text.Encoding.ASCII.GetString(value), + 1 => value.SequenceEqual("0"u8) ? "0" : + value.SequenceEqual("1"u8) ? "1" : System.Text.Encoding.ASCII.GetString(value), + 2 => value.SequenceEqual("br"u8) ? "br" : System.Text.Encoding.ASCII.GetString(value), + 4 => value.SequenceEqual("gzip"u8) ? "gzip" : + value.SequenceEqual("none"u8) ? "none" : System.Text.Encoding.ASCII.GetString(value), + 5 => value.SequenceEqual("close"u8) ? "close" : + value.SequenceEqual("bytes"u8) ? "bytes" : System.Text.Encoding.ASCII.GetString(value), + 6 => value.SequenceEqual("public"u8) ? "public" : System.Text.Encoding.ASCII.GetString(value), + 7 => value.SequenceEqual("chunked"u8) ? "chunked" : + value.SequenceEqual("deflate"u8) ? "deflate" : + value.SequenceEqual("private"u8) ? "private" : + value.SequenceEqual("trailer"u8) ? "trailer" : System.Text.Encoding.ASCII.GetString(value), + 8 => value.SequenceEqual("compress"u8) ? "compress" : + value.SequenceEqual("identity"u8) ? "identity" : + value.SequenceEqual("no-cache"u8) ? "no-cache" : + value.SequenceEqual("no-store"u8) ? "no-store" : + value.SequenceEqual("trailers"u8) ? "trailers" : System.Text.Encoding.ASCII.GetString(value), 10 => value.SequenceEqual("keep-alive"u8) ? "keep-alive" : System.Text.Encoding.ASCII.GetString(value), - _ => System.Text.Encoding.ASCII.GetString(value), + _ => System.Text.Encoding.ASCII.GetString(value), }; /// diff --git a/src/TurboHTTP/Streams/Http10Engine.cs b/src/TurboHTTP/Streams/Http10Engine.cs index 82bb28512..6b8b7638e 100644 --- a/src/TurboHTTP/Streams/Http10Engine.cs +++ b/src/TurboHTTP/Streams/Http10Engine.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Streams.Stages.Internal; @@ -9,9 +9,9 @@ namespace TurboHTTP.Streams; internal class Http10Engine : IHttpProtocolEngine { - private readonly Http1EngineOptions _options; + private readonly TurboClientOptions _options; - public Http10Engine(Http1EngineOptions options) + public Http10Engine(TurboClientOptions options) { _options = options; } @@ -20,11 +20,9 @@ public BidiFlow { - var connection = b.Add(new Http10ConnectionStage( - _options.MaxReconnectAttempts, _options.MaxResponseHeadersLength, - _options.MaxResponseDrainSize, _options.ResponseDrainTimeout)); + var connection = b.Add(new Http10ConnectionStage(_options)); - var batchFlow = b.Add(new NetworkBufferBatchStage(_options.MaxBatchWeight)); + var batchFlow = b.Add(new NetworkBufferBatchStage(_options.Http1.MaxBatchWeight)); b.From(connection.OutNetwork).Via(batchFlow); diff --git a/src/TurboHTTP/Streams/Http11Engine.cs b/src/TurboHTTP/Streams/Http11Engine.cs index 6930ad348..b845ac597 100644 --- a/src/TurboHTTP/Streams/Http11Engine.cs +++ b/src/TurboHTTP/Streams/Http11Engine.cs @@ -1,26 +1,18 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages; using TurboHTTP.Streams.Stages.Internal; namespace TurboHTTP.Streams; -internal record Http1EngineOptions( - int MaxPipelineDepth, - int MaxConnectionsPerServer, - int MaxReconnectAttempts, - long MaxBatchWeight, - int MaxResponseHeadersLength, - int MaxResponseDrainSize, - TimeSpan ResponseDrainTimeout); - internal class Http11Engine : IHttpProtocolEngine { - private readonly Http1EngineOptions _options; + private readonly TurboClientOptions _options; + - public Http11Engine(Http1EngineOptions options) + public Http11Engine(TurboClientOptions options) { _options = options; } @@ -29,9 +21,7 @@ public BidiFlow { - var connection = b.Add(new Http11ConnectionStage( - _options.MaxPipelineDepth, _options.MaxReconnectAttempts, _options.MaxResponseHeadersLength, - _options.MaxResponseDrainSize, _options.ResponseDrainTimeout)); + var connection = b.Add(new Http11ConnectionStage(_options)); // NetworkBufferBatchStage coalesces consecutive NetworkBuffer items from the // encoder into fewer, larger writes — reducing Channel.WriteAsync + Socket.WriteAsync @@ -39,7 +29,7 @@ public BidiFlow -/// Factory for creating transport stage flows for a specific HTTP version. -/// Abstracts transport creation (TCP, QUIC, or custom) so that -/// remains transport-agnostic. -/// -/// -/// Implementations encapsulate transport-specific dependencies (connection manager, options, etc.) -/// and expose a single method that returns a flow bridging the protocol -/// engine to the wire. -/// -internal interface ITransportFactory -{ - /// - /// Creates a transport flow connecting protocol output to wire input. - /// - /// - /// A flow that consumes from the protocol engine and - /// produces from the network. - /// - Flow Create(); -} diff --git a/src/TurboHTTP/Streams/Lifecycle/ClientStreamOwner.cs b/src/TurboHTTP/Streams/Lifecycle/ClientStreamOwner.cs index 499cd1560..7b6b1bbe4 100644 --- a/src/TurboHTTP/Streams/Lifecycle/ClientStreamOwner.cs +++ b/src/TurboHTTP/Streams/Lifecycle/ClientStreamOwner.cs @@ -3,9 +3,8 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Transport.Connection; -using TurboHTTP.Transport.Quic; -using TurboHTTP.Transport.Tcp; +using Servus.Akka.IO.Quic; +using Servus.Akka.IO.Tcp; // QuicConnectionManagerActor is guarded on linux/macOS/windows — all desktop platforms. #pragma warning disable CA1416 @@ -24,7 +23,7 @@ namespace TurboHTTP.Streams.Lifecycle; /// and on actor termination (via ). /// /// -internal sealed class ClientStreamOwner : UntypedActor, IWithTimers +internal sealed class ClientStreamOwner : ReceiveActor, IWithTimers { internal sealed record CreateStreamInstance( TurboClientOptions ClientOptions, @@ -39,14 +38,18 @@ internal sealed record StreamInstanceFailed(Exception Reason, int AttemptNumber) internal sealed record Shutdown; - private static readonly TimeSpan[] RetryBackoffs = - [ - TimeSpan.FromMilliseconds(100), - TimeSpan.FromMilliseconds(500), - TimeSpan.FromSeconds(2) - ]; + private static readonly TimeSpan InitialBackoff = TimeSpan.FromMilliseconds(100); + private static readonly TimeSpan MaxBackoff = TimeSpan.FromSeconds(30); + private const double BackoffMultiplier = 2.0; + + private const int MaxRetryAttempts = 10; + + // Exponential backoff: initialBackoff * 2^attempt, capped at MaxBackoff. + private static TimeSpan CalculateBackoff(int attempt) => + TimeSpan.FromMilliseconds( + Math.Min(InitialBackoff.TotalMilliseconds * Math.Pow(BackoffMultiplier, attempt), + MaxBackoff.TotalMilliseconds)); - private const int MaxRetryAttempts = 3; private static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(5); private const string RetryTimerKey = "retry-create"; @@ -68,38 +71,14 @@ internal sealed record Shutdown; public ITimerScheduler Timers { get; set; } = null!; - protected override void OnReceive(object message) + public ClientStreamOwner() { - switch (message) - { - case CreateStreamInstance create: - HandleCreateStreamInstance(create); - break; - - case StreamInstanceFailed failed: - HandleStreamInstanceFailed(failed); - break; - - case Shutdown: - HandleShutdown(); - break; - - case StreamSinkCompleted completed: - HandleStreamSinkCompleted(completed); - break; - - case RetryCreateInstance: - ExecuteRetryCreate(); - break; - - case ShutdownTimeoutExpired: - HandleShutdownTimeout(); - break; - - default: - Unhandled(message); - break; - } + Receive(HandleCreateStreamInstance); + Receive(HandleStreamInstanceFailed); + Receive(_ => HandleShutdown()); + Receive(HandleStreamSinkCompleted); + Receive(_ => ExecuteRetryCreate()); + Receive(_ => HandleShutdownTimeout()); } private void HandleCreateStreamInstance(CreateStreamInstance create) @@ -138,13 +117,13 @@ private void MaterializeStream(CreateStreamInstance create) "quic-pool"); // Build transport registry and engine flow - var tcpFactory = new TcpTransportFactory(_tcpConnectionManager, create.ClientOptions); + var tcpFactory = new TcpTransportFactory(_tcpConnectionManager); var transports = new TransportRegistry() .Register(new Version(1, 0), tcpFactory) .Register(new Version(1, 1), tcpFactory) .Register(new Version(2, 0), tcpFactory) .Register(new Version(3, 0), new QuicTransportFactory(_quicConnectionManager, - create.ClientOptions, create.ClientOptions.Http3.AllowConnectionMigration)); + create.ClientOptions.Http3.AllowConnectionMigration)); var engine = new Engine(); var engineFlow = engine.CreateFlow( @@ -221,7 +200,7 @@ private void HandleMaterializationFailed(Exception ex) if (_retryAttempts <= MaxRetryAttempts && _createRequest is not null && !_shuttingDown) { - var backoff = RetryBackoffs[Math.Min(_retryAttempts - 1, RetryBackoffs.Length - 1)]; + var backoff = CalculateBackoff(_retryAttempts - 1); _log.Info("Scheduling retry attempt {0} after {1}ms backoff", _retryAttempts, backoff.TotalMilliseconds); @@ -252,7 +231,7 @@ private void HandleStreamInstanceFailed(StreamInstanceFailed failed) if (_retryAttempts < MaxRetryAttempts && _createRequest is not null && !_shuttingDown) { - var backoff = RetryBackoffs[Math.Min(_retryAttempts, RetryBackoffs.Length - 1)]; + var backoff = CalculateBackoff(_retryAttempts); _retryAttempts++; _log.Info("Scheduling retry attempt {0} after {1}ms backoff", _retryAttempts, backoff.TotalMilliseconds); diff --git a/src/TurboHTTP/Streams/ProtocolCoreBuilder.cs b/src/TurboHTTP/Streams/ProtocolCoreBuilder.cs index 5b6400879..bde45e4f1 100644 --- a/src/TurboHTTP/Streams/ProtocolCoreBuilder.cs +++ b/src/TurboHTTP/Streams/ProtocolCoreBuilder.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams.Stages.Internal; namespace TurboHTTP.Streams; @@ -24,13 +24,9 @@ internal static Flow Build( // sustained throughput peaks without excessive memory. var highThroughputBuffer = Attributes.CreateInputBuffer(64, 256); - var http1Options = clientOptions.Http1.ToEngineOptions(); - var http2Options = clientOptions.Http2.ToEngineOptions(); - var http3Options = clientOptions.Http3.ToEngineOptions(); - - var maxConnsH1 = http1Options.MaxConnectionsPerServer; - var maxConnsH2 = http2Options.MaxConnectionsPerServer; - var h2Streams = http2Options.InitialConcurrentStreams; + var maxConnsH1 = clientOptions.Http1.MaxConnectionsPerServer; + var maxConnsH2 = clientOptions.Http2.MaxConnectionsPerServer; + var h2Streams = clientOptions.Http2.MaxConcurrentStreams; var maxConnsH3 = clientOptions.Http3.MaxConnectionsPerServer; @@ -61,10 +57,10 @@ Flow CreateFlowForEndpoint(Req var version = endpoint.Version; IHttpProtocolEngine engine = version switch { - { Major: 1, Minor: 0 } => new Http10Engine(http1Options), - { Major: 1, Minor: 1 } => new Http11Engine(http1Options), - { Major: 2, Minor: 0 } => new Http20Engine(http2Options), - { Major: 3, Minor: 0 } => new Http30Engine(http3Options), + { Major: 1, Minor: 0 } => new Http10Engine(clientOptions), + { Major: 1, Minor: 1 } => new Http11Engine(clientOptions), + { Major: 2, Minor: 0 } => new Http20Engine(clientOptions), + { Major: 3, Minor: 0 } => new Http30Engine(clientOptions), _ => throw new ArgumentOutOfRangeException(nameof(version), version, $"Unsupported HTTP version: {version}") }; diff --git a/src/TurboHTTP/Streams/Stages/ConnectionShape.cs b/src/TurboHTTP/Streams/Stages/ConnectionShape.cs index af16f69ea..9340f0f22 100644 --- a/src/TurboHTTP/Streams/Stages/ConnectionShape.cs +++ b/src/TurboHTTP/Streams/Stages/ConnectionShape.cs @@ -1,10 +1,10 @@ using System.Collections.Immutable; using Akka.Streams; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages; -internal sealed class ConnectionShape: Shape +internal sealed class ConnectionShape : Shape { public Inlet InServer { get; } public Outlet OutResponse { get; } diff --git a/src/TurboHTTP/Streams/Stages/Features/CacheBidiStage.cs b/src/TurboHTTP/Streams/Stages/Features/CacheBidiStage.cs index 7b4646421..6c46b8ef5 100644 --- a/src/TurboHTTP/Streams/Stages/Features/CacheBidiStage.cs +++ b/src/TurboHTTP/Streams/Stages/Features/CacheBidiStage.cs @@ -269,14 +269,14 @@ public void OnStageActorMessage(object message) switch (message) { case BodyReadComplete msg: - { - var request = msg.Response.RequestMessage!; - var now = DateTimeOffset.UtcNow; - _store!.Put(request, msg.Response, msg.Owner, msg.Length, now, now); - FlushPendingCacheResponse(); - DecrementPendingAsync(); - break; - } + { + var request = msg.Response.RequestMessage!; + var now = DateTimeOffset.UtcNow; + _store!.Put(request, msg.Response, msg.Owner, msg.Length, now, now); + FlushPendingCacheResponse(); + DecrementPendingAsync(); + break; + } case BodyReadFailed msg: _ops.Log.Warning("CacheBidiStage: Async body read failed: {0}", msg.Exception.Message); @@ -380,13 +380,11 @@ private void EmitCacheTelemetry(HttpRequestMessage request, bool isHit) if (isHit) { TurboHttpMetrics.CacheHit.Add(1); - TurboHttpEventSource.Instance.CacheHit(uri); TurboTrace.Cache.Info(_ops, "Cache hit: {0}", uri); } else { TurboHttpMetrics.CacheMiss.Add(1); - TurboHttpEventSource.Instance.CacheMiss(uri); TurboTrace.Cache.Info(_ops, "Cache miss: {0}", uri); } } diff --git a/src/TurboHTTP/Streams/Stages/Features/RedirectBidiStage.cs b/src/TurboHTTP/Streams/Stages/Features/RedirectBidiStage.cs index cc4f91099..1cde806ae 100644 --- a/src/TurboHTTP/Streams/Stages/Features/RedirectBidiStage.cs +++ b/src/TurboHTTP/Streams/Stages/Features/RedirectBidiStage.cs @@ -298,9 +298,6 @@ public void OnResponse(HttpResponseMessage response) TurboHttpMetrics.RedirectCount.Add(1, new KeyValuePair("http.response.status_code", (int)response.StatusCode)); - TurboHttpEventSource.Instance.Redirect( - (int)response.StatusCode, - newRequest.RequestUri?.OriginalString ?? ""); TurboTrace.Redirect.Info(_ops, "Redirect followed: {0} → {2} (HTTP {1})", original.RequestUri?.OriginalString ?? "", (int)response.StatusCode, diff --git a/src/TurboHTTP/Streams/Stages/Features/RetryBidiStage.cs b/src/TurboHTTP/Streams/Stages/Features/RetryBidiStage.cs index b3fbb0f11..23d2715cd 100644 --- a/src/TurboHTTP/Streams/Stages/Features/RetryBidiStage.cs +++ b/src/TurboHTTP/Streams/Stages/Features/RetryBidiStage.cs @@ -362,8 +362,6 @@ private void EmitRetryTelemetry(HttpRequestMessage original, int attemptCount) var retryActivity = TurboHttpInstrumentation.StartRetry(attemptCount); retryActivity?.Stop(); Activity.Current = previous; - - TurboHttpEventSource.Instance.RetryAttempt(attemptCount + 1); TurboHttpMetrics.RetryCount.Add(1, new KeyValuePair("http.request.method", original.Method.Method), new KeyValuePair("server.address", original.RequestUri?.Host ?? "unknown")); diff --git a/src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs b/src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs index 718b09b9c..32a03d5ce 100644 --- a/src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs +++ b/src/TurboHTTP/Streams/Stages/Features/TracingBidiStage.cs @@ -149,11 +149,6 @@ public void OnRequestPush(HttpRequestMessage request) _currentActivity = activity; } - TurboHttpDiagnosticSource.OnRequestStart(request); - TurboHttpEventSource.Instance.RequestStart( - request.Method.Method, - request.RequestUri?.OriginalString ?? ""); - var method = request.Method.Method; var uri = request.RequestUri?.OriginalString ?? ""; TurboTrace.Request.Info(_ops, "Request started: {0} {1}", method, uri); @@ -174,7 +169,6 @@ public void OnRequestPush(HttpRequestMessage request) public void OnRequestUpstreamFailure(Exception ex) { TurboTrace.Request.Warning(_ops, $"Request failed: {ex.GetType().Name} — {ex.Message}"); - TurboHttpEventSource.Instance.RequestFailed("UNKNOWN", "", ex.GetType().Name); if (_currentActivity is not null) { @@ -205,13 +199,6 @@ public void OnResponsePush(HttpResponseMessage response) var statusCode = (int)response.StatusCode; TurboTrace.Request.Info(_ops, "Request completed: {0} ({1:F1}ms)", statusCode, durationMs); - if (request is not null) - { - TurboHttpDiagnosticSource.OnRequestStop(request, response, TaskStatus.RanToCompletion); - } - - TurboHttpEventSource.Instance.RequestStop( - request?.Method.Method ?? "UNKNOWN", statusCode, durationMs); RecordActiveRequestEnd(request); @@ -223,7 +210,6 @@ public void OnResponsePush(HttpResponseMessage response) public void OnResponseUpstreamFailure(Exception ex) { TurboTrace.Request.Warning(_ops, $"Request failed: {ex.GetType().Name} — {ex.Message}"); - TurboHttpEventSource.Instance.RequestFailed("UNKNOWN", "", ex.GetType().Name); if (_currentActivity is not null) { diff --git a/src/TurboHTTP/Streams/Stages/Http10ConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Http10ConnectionStage.cs index 688d5f9df..bfe9c9bc7 100644 --- a/src/TurboHTTP/Streams/Stages/Http10ConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Http10ConnectionStage.cs @@ -1,7 +1,7 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http10; namespace TurboHTTP.Streams.Stages; @@ -12,21 +12,11 @@ internal sealed class Http10ConnectionStage : GraphStage private readonly Outlet _outResponse = new("Http10Connection.Out.Response"); private readonly Inlet _inApp = new("Http10Connection.In.App"); private readonly Outlet _outNetwork = new("Http10Connection.Out.Network"); - private readonly int _maxReconnectAttempts; - private readonly int _maxResponseHeadersLength; - private readonly int _maxResponseDrainSize; - private readonly TimeSpan _responseDrainTimeout; - - public Http10ConnectionStage( - int maxReconnectAttempts = 3, - int maxResponseHeadersLength = 64, - int maxResponseDrainSize = 1024 * 1024, - TimeSpan? responseDrainTimeout = null) + private readonly TurboClientOptions _options; + + public Http10ConnectionStage(TurboClientOptions options) { - _maxReconnectAttempts = maxReconnectAttempts; - _maxResponseHeadersLength = maxResponseHeadersLength; - _maxResponseDrainSize = maxResponseDrainSize; - _responseDrainTimeout = responseDrainTimeout ?? TimeSpan.FromSeconds(2); + _options = options; } public override ConnectionShape Shape => new(_inServer, _outResponse, _inApp, _outNetwork); @@ -48,8 +38,7 @@ public Logic(Http10ConnectionStage stage, Attributes inheritedAttributes) : base _stage = stage; var memoryBuffer = inheritedAttributes.GetAttribute(new TurboAttributes.MemoryBuffer(4 * 1024, 256 * 1024)); - _sm = new StateMachine(this, _stage._maxReconnectAttempts, memoryBuffer.Initial, memoryBuffer.Max, - _stage._maxResponseHeadersLength, _stage._maxResponseDrainSize, _stage._responseDrainTimeout); + _sm = new StateMachine(this, _stage._options, memoryBuffer.Initial, memoryBuffer.Max); SetHandler(stage._inServer, onPush: OnServerPush, onUpstreamFinish: () => @@ -142,58 +131,58 @@ private void OnServerPush() switch (item) { case ConnectedSignalItem: - { - _sm.OnConnectionRestored(); - FlushOutbound(); - TryPullRequest(); - // Pull to receive the response from the new connection - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.OnConnectionRestored(); + FlushOutbound(); + TryPullRequest(); + // Pull to receive the response from the new connection + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } - case CloseSignalItem when _sm.IsReconnecting: - { - _sm.OnReconnectAttemptFailed(); - if (_reconnectFailed) - { - Log.Warning( - "Http10ConnectionStage: Reconnect failed after max attempts — discarding {0} in-flight request(s).", - _sm.PendingRequestCount); - CompleteStage(); return; } - - FlushOutbound(); - // Pull to receive ConnectedSignalItem or next CloseSignalItem - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + case CloseSignalItem when _sm.IsReconnecting: { - Pull(_stage._inServer); - } + _sm.OnReconnectAttemptFailed(); + if (_reconnectFailed) + { + Log.Warning( + "Http10ConnectionStage: Reconnect failed after max attempts — discarding {0} in-flight request(s).", + _sm.PendingRequestCount); + CompleteStage(); + return; + } - return; - } + FlushOutbound(); + // Pull to receive ConnectedSignalItem or next CloseSignalItem + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } + + return; + } case CloseSignalItem when _sm.HasInFlightRequest: - { - _sm.StartReconnect(); - FlushOutbound(); - // Pull to receive ConnectedSignalItem from the reconnected transport - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.StartReconnect(); + FlushOutbound(); + // Pull to receive ConnectedSignalItem from the reconnected transport + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } case CloseSignalItem: - { - // Connection closed with no in-flight request and no reconnect pending. - // App upstream is either already finished or will complete via onUpstreamFinish. - CompleteStage(); - return; - } + { + // Connection closed with no in-flight request and no reconnect pending. + // App upstream is either already finished or will complete via onUpstreamFinish. + CompleteStage(); + return; + } } try diff --git a/src/TurboHTTP/Streams/Stages/Http11ConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Http11ConnectionStage.cs index 7a3f4e5af..e66d93af6 100644 --- a/src/TurboHTTP/Streams/Stages/Http11ConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Http11ConnectionStage.cs @@ -1,7 +1,7 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http11; namespace TurboHTTP.Streams.Stages; @@ -13,24 +13,11 @@ internal sealed class Http11ConnectionStage : GraphStage private readonly Inlet _inApp = new("Http11Connection.In.App"); private readonly Outlet _outNetwork = new("Http11Connection.Out.Network"); - private readonly int _maxPipelineDepth; - private readonly int _maxReconnectAttempts; - private readonly int _maxResponseHeadersLength; - private readonly int _maxResponseDrainSize; - private readonly TimeSpan _responseDrainTimeout; - - public Http11ConnectionStage( - int maxPipelineDepth = 8, - int maxReconnectAttempts = 3, - int maxResponseHeadersLength = 64, - int maxResponseDrainSize = 1024 * 1024, - TimeSpan? responseDrainTimeout = null) + private readonly TurboClientOptions _options; + + public Http11ConnectionStage(TurboClientOptions options) { - _maxPipelineDepth = maxPipelineDepth; - _maxReconnectAttempts = maxReconnectAttempts; - _maxResponseHeadersLength = maxResponseHeadersLength; - _maxResponseDrainSize = maxResponseDrainSize; - _responseDrainTimeout = responseDrainTimeout ?? TimeSpan.FromSeconds(2); + _options = options; } public override ConnectionShape Shape => new(_inServer, _outResponse, _inApp, _outNetwork); @@ -53,9 +40,7 @@ public Logic(Http11ConnectionStage stage, Attributes inheritedAttributes) : base _stage = stage; var memoryBuffer = inheritedAttributes.GetAttribute(new TurboAttributes.MemoryBuffer(4 * 1024, 256 * 1024)); - _sm = new StateMachine(this, stage._maxPipelineDepth, stage._maxReconnectAttempts, - memoryBuffer.Initial, memoryBuffer.Max, stage._maxResponseHeadersLength, - stage._maxResponseDrainSize, stage._responseDrainTimeout); + _sm = new StateMachine(this, stage._options, memoryBuffer.Initial, memoryBuffer.Max); SetHandler(stage._inServer, onPush: OnServerPush, onUpstreamFinish: () => diff --git a/src/TurboHTTP/Streams/Stages/Http20ConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Http20ConnectionStage.cs index a1e95d8e6..86a92ff0c 100644 --- a/src/TurboHTTP/Streams/Stages/Http20ConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Http20ConnectionStage.cs @@ -1,8 +1,8 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; +using Servus.Akka.IO; using TurboHTTP.Diagnostics; -using TurboHTTP.Internal; using TurboHTTP.Protocol.Http2; namespace TurboHTTP.Streams.Stages; @@ -13,11 +13,11 @@ internal sealed class Http20ConnectionStage : GraphStage private readonly Outlet _outResponse = new("Http20Connection.Out.Response"); private readonly Inlet _inApp = new("Http20Connection.In.App"); private readonly Outlet _outNetwork = new("Http20Connection.Out.Network"); - private readonly Http2EngineOptions _options; + private readonly TurboClientOptions _options; public override ConnectionShape Shape => new(_inServer, _outResponse, _inApp, _outNetwork); - public Http20ConnectionStage(Http2EngineOptions options) + public Http20ConnectionStage(TurboClientOptions options) { _options = options; } @@ -41,7 +41,7 @@ public Logic(Http20ConnectionStage stage) : base(stage.Shape) { _stage = stage; _sm = new StateMachine(stage._options, this); - _keepAliveEnabled = stage._options.KeepAlivePingDelay != Timeout.InfiniteTimeSpan; + _keepAliveEnabled = stage._options.Http2.KeepAlivePingDelay != Timeout.InfiniteTimeSpan; SetHandler(stage._inServer, onPush: OnServerPush, onUpstreamFinish: () => @@ -115,49 +115,49 @@ private void OnServerPush() { // Reconnect: new connection ready — replay buffered requests case ConnectedSignalItem: - { - _sm.OnConnectionRestored(); - FlushOutbound(); - ScheduleKeepAlivePing(); - TryPullRequest(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.OnConnectionRestored(); + FlushOutbound(); + ScheduleKeepAlivePing(); + TryPullRequest(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } // Reconnect: connection dropped again while already reconnecting case CloseSignalItem when _sm.IsReconnecting: - { - _sm.OnReconnectAttemptFailed(); - if (_reconnectFailed) { - FailStage(new HttpRequestException( - "TurboHTTP: HTTP/2 reconnect failed after max attempts.")); - return; - } + _sm.OnReconnectAttemptFailed(); + if (_reconnectFailed) + { + FailStage(new HttpRequestException( + "TurboHTTP: HTTP/2 reconnect failed after max attempts.")); + return; + } - FlushOutbound(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) - { - Pull(_stage._inServer); - } + FlushOutbound(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } // Reconnect: abrupt close with in-flight requests (no GOAWAY) case CloseSignalItem when _sm.HasInFlightRequests: - { - _sm.OnConnectionLost(lastStreamId: 0); - FlushOutbound(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.OnConnectionLost(lastStreamId: 0); + FlushOutbound(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } // CloseSignalItem with no in-flight — complete normally case CloseSignalItem: CompleteStage(); @@ -227,36 +227,36 @@ protected override void OnTimer(object timerKey) switch (timerKey) { case KeepAlivePingTimerKey: - { - var policy = _stage._options.KeepAlivePingPolicy; - if (policy == HttpKeepAlivePingPolicy.WithActiveRequests && !_sm.HasInFlightRequests) { - return; - } + var policy = _stage._options.Http2.KeepAlivePingPolicy; + if (policy == HttpKeepAlivePingPolicy.WithActiveRequests && !_sm.HasInFlightRequests) + { + return; + } - _sm.SendKeepAlivePing(); - FlushOutbound(); - ScheduleKeepAlivePingTimeout(); - break; - } + _sm.SendKeepAlivePing(); + FlushOutbound(); + ScheduleKeepAlivePingTimeout(); + break; + } case KeepAlivePingTimeoutKey: - { - if (_sm.IsKeepAliveTimedOut(_stage._options.KeepAlivePingTimeout)) { - Log.Warning("Http20ConnectionStage: Keep-alive PING timeout — closing connection."); - if (_sm.HasInFlightRequests) + if (_sm.IsKeepAliveTimedOut(_stage._options.Http2.KeepAlivePingTimeout)) { - _sm.OnConnectionLost(lastStreamId: 0); - FlushOutbound(); + Log.Warning("Http20ConnectionStage: Keep-alive PING timeout — closing connection."); + if (_sm.HasInFlightRequests) + { + _sm.OnConnectionLost(lastStreamId: 0); + FlushOutbound(); + } + else + { + CompleteStage(); + } } - else - { - CompleteStage(); - } - } - break; - } + break; + } } } @@ -264,7 +264,7 @@ private void ScheduleKeepAlivePing() { if (_keepAliveEnabled) { - ScheduleOnce(KeepAlivePingTimerKey, _stage._options.KeepAlivePingDelay); + ScheduleOnce(KeepAlivePingTimerKey, _stage._options.Http2.KeepAlivePingDelay); } } @@ -272,7 +272,7 @@ private void ScheduleKeepAlivePingTimeout() { if (_keepAliveEnabled) { - ScheduleOnce(KeepAlivePingTimeoutKey, _stage._options.KeepAlivePingTimeout); + ScheduleOnce(KeepAlivePingTimeoutKey, _stage._options.Http2.KeepAlivePingTimeout); } } diff --git a/src/TurboHTTP/Streams/Stages/Http30ConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Http30ConnectionStage.cs index e28ce0968..c0e62808c 100644 --- a/src/TurboHTTP/Streams/Stages/Http30ConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Http30ConnectionStage.cs @@ -1,7 +1,7 @@ using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Protocol.Http3; namespace TurboHTTP.Streams.Stages; @@ -13,9 +13,9 @@ internal sealed class Http30ConnectionStage : GraphStage private readonly Inlet _inApp = new("Http30Connection.In.App"); private readonly Outlet _outNetwork = new("Http30Connection.Out.Network"); - private readonly Http3EngineOptions _options; + private readonly TurboClientOptions _options; - public Http30ConnectionStage(Http3EngineOptions options) + public Http30ConnectionStage(TurboClientOptions options) { _options = options; } @@ -95,6 +95,12 @@ public Logic(Http30ConnectionStage stage) : base(stage.Shape) public override void PreStart() { + EmitMultiple(_stage._outNetwork, [ + new OpenTypedStreamItem(0x00, -2, Outbound: true), + new OpenTypedStreamItem(0x02, -3, Outbound: true), + new OpenTypedStreamItem(0x03, -4, Outbound: false), + new ProtocolReadyItem(), + ]); ScheduleIdleCheck(); } @@ -109,11 +115,11 @@ protected override void OnTimer(object timerKey) if (goAway is not null) { // Serialize and emit the GOAWAY frame - var buf = Http3NetworkBuffer.Rent(goAway.SerializedSize); + var buf = RoutedNetworkBuffer.Rent(goAway.SerializedSize); var span = buf.FullMemory.Span; goAway.WriteTo(ref span); buf.Length = goAway.SerializedSize; - buf.StreamType = Http3StreamType.Control; + buf.StreamTypeValue = (long)StreamType.Control; _pendingOutbound.Add(buf); FlushOutbound(); CompleteStage(); @@ -153,7 +159,7 @@ private void OnServerPush() case QuicCloseItem: HandleSignalItem(item); return; - case Http3NetworkBuffer tagged when tagged.StreamType != Http3StreamType.None: + case RoutedNetworkBuffer tagged: HandleTaggedStreamData(tagged); return; case NetworkBuffer rawBuffer: @@ -174,64 +180,64 @@ private void HandleSignalItem(IInputItem item) { // Reconnect: new connection ready — replay buffered requests case ConnectedSignalItem: - { - _sm.OnConnectionRestored(); - FlushOutbound(); - TryPullRequest(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.OnConnectionRestored(); + FlushOutbound(); + TryPullRequest(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } // Request stream FIN — server finished sending the response. case QuicCloseItem { Kind: QuicCloseKind.RequestStreamComplete } close: - { - if (close.StreamId >= 0) - { - _sm.FlushPendingResponse(close.StreamId); - } - else { - _sm.FlushPendingResponse(); + if (close.StreamId >= 0) + { + _sm.FlushPendingResponse(close.StreamId); + } + else + { + _sm.FlushPendingResponse(); + } + + FlushResponses(); + TryPullRequest(); + return; } - - FlushResponses(); - TryPullRequest(); - return; - } // Reconnect: connection dropped again while already reconnecting case QuicCloseItem when _sm.IsReconnecting: - { - _sm.OnReconnectAttemptFailed(); - if (_reconnectFailed) { - FailStage(new HttpRequestException( - "TurboHTTP: HTTP/3 reconnect failed after max attempts.")); - return; - } + _sm.OnReconnectAttemptFailed(); + if (_reconnectFailed) + { + FailStage(new HttpRequestException( + "TurboHTTP: HTTP/3 reconnect failed after max attempts.")); + return; + } + + FlushOutbound(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - FlushOutbound(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) - { - Pull(_stage._inServer); + return; } - - return; - } // Abrupt close with in-flight requests — reconnect case QuicCloseItem when _sm.HasInFlightRequests: - { - _sm.OnConnectionLost(); - FlushOutbound(); - if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) { - Pull(_stage._inServer); - } + _sm.OnConnectionLost(); + FlushOutbound(); + if (!HasBeenPulled(_stage._inServer) && !IsClosed(_stage._inServer)) + { + Pull(_stage._inServer); + } - return; - } + return; + } // QuicCloseItem with no in-flight — complete normally case QuicCloseItem: CompleteStage(); @@ -239,34 +245,43 @@ private void HandleSignalItem(IInputItem item) } } - private void HandleTaggedStreamData(Http3NetworkBuffer tagged) + private void HandleTaggedStreamData(RoutedNetworkBuffer tagged) { - switch (tagged.StreamType) + StreamType? type = tagged switch { - case Http3StreamType.QpackDecoder: - { - _sm.ProcessQpackDecoderBytes(tagged.Memory); - tagged.Dispose(); - Pull(_stage._inServer); - return; - } - case Http3StreamType.QpackEncoder: - { - _sm.ProcessQpackEncoderBytes(tagged.Memory); - tagged.Dispose(); - Pull(_stage._inServer); - return; - } - // Control stream — decode frames for SETTINGS/GOAWAY but use a dedicated stream ID - // to keep control-stream remainder state separate from request streams. - case Http3StreamType.Control: + { StreamTypeValue: (long)StreamType.Control } => StreamType.Control, + { StreamTypeValue: (long)StreamType.QpackEncoder } => StreamType.QpackEncoder, + { StreamTypeValue: (long)StreamType.QpackDecoder } => StreamType.QpackDecoder, + { StreamTypeValue: null } => null, + _ => throw new ArgumentOutOfRangeException(nameof(tagged), tagged, null) + }; + + switch (type) + { + case StreamType.QpackDecoder: + { + _sm.ProcessQpackDecoderBytes(tagged.Memory); + tagged.Dispose(); + Pull(_stage._inServer); + return; + } + case StreamType.QpackEncoder: + { + _sm.ProcessQpackEncoderBytes(tagged.Memory); + tagged.Dispose(); + Pull(_stage._inServer); + return; + } + case StreamType.Control: ProcessFrameData(tagged, streamId: ControlStreamDecoderId); return; + case StreamType.Push: + break; default: - { - ProcessFrameData(tagged, tagged.StreamId); - return; - } + { + ProcessFrameData(tagged, tagged.StreamId!.Value); + return; + } } } diff --git a/src/TurboHTTP/Streams/Stages/IStageOperations.cs b/src/TurboHTTP/Streams/Stages/IStageOperations.cs index 0fd6ae115..04ab76e0f 100644 --- a/src/TurboHTTP/Streams/Stages/IStageOperations.cs +++ b/src/TurboHTTP/Streams/Stages/IStageOperations.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages; diff --git a/src/TurboHTTP/Streams/Stages/Internal/EndpointDispatchStage.cs b/src/TurboHTTP/Streams/Stages/Internal/EndpointDispatchStage.cs index 9f54fc22b..cf2d6e67b 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/EndpointDispatchStage.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/EndpointDispatchStage.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages.Internal; diff --git a/src/TurboHTTP/Streams/Stages/Internal/GroupByExtensions.cs b/src/TurboHTTP/Streams/Stages/Internal/GroupByExtensions.cs index 32eab8883..1311d4998 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/GroupByExtensions.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/GroupByExtensions.cs @@ -2,7 +2,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.Implementation; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages.Internal; diff --git a/src/TurboHTTP/Streams/Stages/Internal/GroupByRequestEndpointStage.cs b/src/TurboHTTP/Streams/Stages/Internal/GroupByRequestEndpointStage.cs index e1299df9d..870140a34 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/GroupByRequestEndpointStage.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/GroupByRequestEndpointStage.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages.Internal; diff --git a/src/TurboHTTP/Streams/Stages/Internal/HostKeyMergeBack.cs b/src/TurboHTTP/Streams/Stages/Internal/HostKeyMergeBack.cs index 0eb1c10be..38dba15a9 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/HostKeyMergeBack.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/HostKeyMergeBack.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams.Dsl; using Akka.Streams.Implementation; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages.Internal; diff --git a/src/TurboHTTP/Streams/Stages/Internal/MergeSubstreamsStage.cs b/src/TurboHTTP/Streams/Stages/Internal/MergeSubstreamsStage.cs index ecc81c865..38bc1e408 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/MergeSubstreamsStage.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/MergeSubstreamsStage.cs @@ -141,7 +141,7 @@ private void MaterializeSubstream(Source source) if (IsAvailable(_stage._out)) { var elem = subSink.Grab(); - + Push(_stage._out, elem); subSink.Pull(); } diff --git a/src/TurboHTTP/Streams/Stages/Internal/NetworkBufferBatchStage.cs b/src/TurboHTTP/Streams/Stages/Internal/NetworkBufferBatchStage.cs index bc5e3a333..9868580e7 100644 --- a/src/TurboHTTP/Streams/Stages/Internal/NetworkBufferBatchStage.cs +++ b/src/TurboHTTP/Streams/Stages/Internal/NetworkBufferBatchStage.cs @@ -1,6 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; -using TurboHTTP.Internal; +using Servus.Akka.IO; namespace TurboHTTP.Streams.Stages.Internal; diff --git a/src/TurboHTTP/Streams/Stages/RequestEnricher.cs b/src/TurboHTTP/Streams/Stages/RequestEnricher.cs index 40e5ef74d..c04026800 100644 --- a/src/TurboHTTP/Streams/Stages/RequestEnricher.cs +++ b/src/TurboHTTP/Streams/Stages/RequestEnricher.cs @@ -59,10 +59,8 @@ public HttpRequestMessage Enrich(HttpRequestMessage request) } } - // Rule 4 removed: RFC 9110 §6.6.1 — clients SHOULD NOT send Date. - // Rule 5: PreAuthenticate — inject Authorization header when credentials are available - if (options.PreAuthenticate && options.Credentials is not null && !request.Headers.Contains("Authorization")) + if (options is { PreAuthenticate: true, Credentials: not null } && !request.Headers.Contains("Authorization")) { InjectAuthorization(request, options.Credentials); } diff --git a/src/TurboHTTP/Streams/TransportRegistry.cs b/src/TurboHTTP/Streams/TransportRegistry.cs index 9627c59ee..a1c76eb57 100644 --- a/src/TurboHTTP/Streams/TransportRegistry.cs +++ b/src/TurboHTTP/Streams/TransportRegistry.cs @@ -1,7 +1,7 @@ using Akka; using Akka.Streams.Dsl; -using TurboHTTP.Internal; using System.Net; +using Servus.Akka.IO; namespace TurboHTTP.Streams; @@ -54,4 +54,4 @@ public Flow Get(Version version) $"No transport factory registered for HTTP version {version}. " + $"Registered versions: {string.Join(", ", _transports.Keys)}"); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/AbruptCloseException.cs b/src/TurboHTTP/Transport/Connection/AbruptCloseException.cs deleted file mode 100644 index be5170158..000000000 --- a/src/TurboHTTP/Transport/Connection/AbruptCloseException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TurboHTTP.Transport.Connection; - -/// -/// Signals that the transport connection was closed abruptly (no TLS close_notify, TCP RST, or I/O error). -/// Used to complete the inbound channel so that can distinguish -/// clean TLS closure from abrupt disconnection. -/// -internal sealed class AbruptCloseException() - : TurboTransportException("Connection closed abruptly without TLS close_notify"); \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Connection/IConnectionFactory.cs b/src/TurboHTTP/Transport/Connection/IConnectionFactory.cs deleted file mode 100644 index eda2da4b8..000000000 --- a/src/TurboHTTP/Transport/Connection/IConnectionFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TurboHTTP.Internal; - -namespace TurboHTTP.Transport.Connection; - -internal interface IConnectionFactory -{ - Task EstablishAsync(TcpOptions options, RequestEndpoint endpoint, CancellationToken ct); -} diff --git a/src/TurboHTTP/Transport/Quic/IQuicTransportEvent.cs b/src/TurboHTTP/Transport/Quic/IQuicTransportEvent.cs deleted file mode 100644 index baf7979e1..000000000 --- a/src/TurboHTTP/Transport/Quic/IQuicTransportEvent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; - -namespace TurboHTTP.Transport.Quic; - -internal interface IQuicTransportEvent; - -internal readonly record struct ConnectionLeaseAcquired(QuicConnectionLease Lease) : IQuicTransportEvent; - -internal readonly record struct RequestLeaseAcquired(ConnectionLease Lease, long StreamId) : IQuicTransportEvent; - -internal readonly record struct TypedLeaseAcquired(ConnectionLease Lease, Http3StreamType StreamType) : IQuicTransportEvent; - -internal readonly record struct AcquisitionFailed(Exception Error) : IQuicTransportEvent; - -internal readonly record struct InboundData(IInputItem Item, int Gen) : IQuicTransportEvent; - -internal readonly record struct InboundComplete(TlsCloseKind CloseKind, int Gen, long StreamId = -1) : IQuicTransportEvent; - -internal readonly record struct InboundPumpFailed(Exception Error, long StreamId = -1) : IQuicTransportEvent; - -internal readonly record struct InboundStreamReady(QuicConnectionHandle.InboundStream Stream) : IQuicTransportEvent; - -internal readonly record struct OutboundWriteDone : IQuicTransportEvent; - -internal readonly record struct OutboundWriteFailed(Exception Error) : IQuicTransportEvent; - -internal readonly record struct EarlyDataRejected(NetworkBuffer Buffer) : IQuicTransportEvent; - -internal readonly record struct ConnectionMigrated( - System.Net.EndPoint? OldLocalEndPoint, - System.Net.EndPoint? NewLocalEndPoint) : IQuicTransportEvent; \ No newline at end of file diff --git a/src/TurboHTTP/Transport/Tcp/TcpTransportEvent.cs b/src/TurboHTTP/Transport/Tcp/TcpTransportEvent.cs deleted file mode 100644 index d18c2581c..000000000 --- a/src/TurboHTTP/Transport/Tcp/TcpTransportEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using TurboHTTP.Internal; -using TurboHTTP.Transport.Connection; - -namespace TurboHTTP.Transport.Tcp; - -internal readonly record struct LeaseAcquired(ConnectionLease Lease) : ITcpTransportEvent; - -internal readonly record struct AcquisitionFailed(Exception Error) : ITcpTransportEvent; - -internal readonly record struct InboundBatch(IInputItem[] Batch, int Count, int Gen) : ITcpTransportEvent; - -internal readonly record struct InboundComplete(TlsCloseKind CloseKind, int Gen) : ITcpTransportEvent; - -internal readonly record struct InboundPumpFailed(Exception Error) : ITcpTransportEvent; - -internal readonly record struct OutboundWriteDone : ITcpTransportEvent; - -internal readonly record struct OutboundWriteFailed(Exception Error) : ITcpTransportEvent; - -internal readonly record struct FlushNextCompleted : ITcpTransportEvent; - -internal interface ITcpTransportEvent; \ No newline at end of file diff --git a/src/TurboHTTP/TurboHTTP.csproj b/src/TurboHTTP/TurboHTTP.csproj index 279849a2b..84b69a277 100644 --- a/src/TurboHTTP/TurboHTTP.csproj +++ b/src/TurboHTTP/TurboHTTP.csproj @@ -43,4 +43,7 @@ + + + diff --git a/src/TurboHTTP/TurboHttpClient.cs b/src/TurboHTTP/TurboHttpClient.cs index 73c0a6256..2c0d79187 100644 --- a/src/TurboHTTP/TurboHttpClient.cs +++ b/src/TurboHTTP/TurboHttpClient.cs @@ -4,7 +4,7 @@ using System.Threading.Channels; using System.Threading.Tasks.Sources; using Akka.Actor; -using TurboHTTP.Internal; +using Servus.Akka.IO; using TurboHTTP.Streams; using TurboHTTP.Streams.Lifecycle; diff --git a/src/TurboHTTP/packages.lock.json b/src/TurboHTTP/packages.lock.json index 92c9367bd..96925c2ad 100644 --- a/src/TurboHTTP/packages.lock.json +++ b/src/TurboHTTP/packages.lock.json @@ -34,17 +34,6 @@ "resolved": "3.0.1", "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" }, - "Servus.Akka": { - "type": "Direct", - "requested": "[0.3.10, )", - "resolved": "0.3.10", - "contentHash": "tO2i3rAtZe1rgsY0ka7ZIucQvimz2tQsFGlWoznPONuv2czSHI58NwBdfPyv2OVaRRojJND8+DBrksInlxWmiw==", - "dependencies": { - "Akka.Hosting": "1.5.0", - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Servus.Core": "0.33.1" - } - }, "Akka": { "type": "Transitive", "resolved": "1.5.65", @@ -86,10 +75,10 @@ }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "g23//mPpMa33QdJkLujJICoCRbiLFpiQ4XbROG9JdeDI6/sM+qZPB2t5SmUWNM8GwY8dYW3NucxlZDFe8s3NAQ==", + "resolved": "8.0.0", + "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.11" + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { @@ -110,16 +99,16 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "+ZxxZzcVU+IEzq12GItUzf/V3mEc5nSLiXijwvDc4zyhbjvSZZ043giSZqGnhakrjwRWjkerIHPrRwm9okEIpw==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "D9gu4weEmvWGuz8zp5xwsOr0ldmWphMKr7+IW66hG4rnrgpMLtTWoOINBOX5mcRTPL39+AVd3BJdc4HTvl2NrA==", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.11", - "Microsoft.Extensions.Options": "9.0.11" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.HealthChecks": { @@ -140,22 +129,22 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "YEPsXWcoNde6J6W/MMjIuNQMPkKTL4NS0AJ1rsAt48+GuJYoZU+Mi4T8PwyzYGDLxhUsH3Wa32DlbKtDkzT40A==", + "resolved": "8.0.0", + "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.11" + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "cQsyEUYRYRzRf4y7Xn4W8bbspgXj0oNA9drEa6lVmU9qL7xv2dfCdcVVLCp6Hhs8hN7R7TfRFdQa1uXBS+96fA==", + "resolved": "8.0.0", + "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.11", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.11", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.11", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.11", - "Microsoft.Extensions.Logging.Abstractions": "9.0.11" + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging": { @@ -170,10 +159,10 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "UKWFTDwtZQIoypyt1YPVsxTnDK+0sKn26+UeSGeNlkRQddrkt9EC6kP4g94rgO/WOZkz94bKNlF1dVZN3QfPFQ==", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.11" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -198,11 +187,11 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "HX4M3BLkW1dtByMKHDVq6r7Jy6e4hf8NDzHpIgz7C8BtYk9JQHhfYX5c1UheQTD5Veg1yBhz/cD9C8vtrGrk9w==", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.11", - "Microsoft.Extensions.Primitives": "9.0.11" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -219,8 +208,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.11", - "contentHash": "rtUNSIhbQTv8iSBTFvtg2b/ZUkoqC9qAH9DdC2hr+xPpoZrxiCITci9UR/ELUGUGnGUrF8Xye+tGVRhCxE+4LA==" + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", @@ -261,15 +250,6 @@ "resolved": "1.0.4", "contentHash": "k5lcLBmVAcc1OMGsLa5cxdzJazNx9haOb9GUINx+DulBqEvRAHCgxhDWcJjm44Pe633xwdKOOYGQhPSdj944IA==" }, - "Servus.Core": { - "type": "Transitive", - "resolved": "0.33.1", - "contentHash": "j76OHV7QaQINH639cHI9xh4wBSrNrQWUZgjka+RmlvhKQXKpQZTz7dRlXa5rdDGEUKn/pkf49MC65f092pqdLA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.11", - "Microsoft.Extensions.Hosting.Abstractions": "9.0.11" - } - }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.1", @@ -307,6 +287,12 @@ "dependencies": { "System.Drawing.Common": "6.0.0" } + }, + "servus.akka": { + "type": "Project", + "dependencies": { + "Akka.Hosting": "[1.5.65, )" + } } } }