|
1 | 1 | <div align="center"> |
2 | 2 | <img src="docs/logo/logo.svg" alt="TurboHTTP" width="200" /> |
3 | | - <p><strong>High-performance HTTP client for .NET — built on Akka.Streams with automatic retries, caching, cookies, HTTP/2 multiplexing, and HTTP/3 (QUIC).</strong></p> |
| 3 | + <p><strong>High-performance HTTP client and server for .NET — built on Akka.Streams with full protocol support from HTTP/1.0 through HTTP/3 (QUIC).</strong></p> |
4 | 4 |
|
5 | 5 | [](https://github.com/st0o0/TurboHTTP/actions/workflows/ci.yml) |
6 | 6 | [](https://github.com/st0o0/TurboHTTP/actions/workflows/release.yml) |
|
12 | 12 |
|
13 | 13 | ## Why TurboHTTP? |
14 | 14 |
|
15 | | -TurboHTTP replaces `HttpClient` with a reactive, backpressure-aware HTTP pipeline built on [Akka.Streams](https://getakka.net/). Actors manage connection lifecycle while data flows through `System.Threading.Channels` — zero bytes ever touch an actor mailbox. The result: high throughput, low allocations, and a pipeline that never dies on transient errors. |
| 15 | +TurboHTTP is a reactive, backpressure-aware HTTP stack built on [Akka.Streams](https://getakka.net/). Actors manage connection lifecycle while data flows through `System.Threading.Channels` — zero bytes ever touch an actor mailbox. Both the client and the server share the same protocol layer, transport, and stream pipeline, giving you a symmetric architecture from HTTP/1.0 through HTTP/3. The result: high throughput, low allocations, and a pipeline that never dies on transient errors. |
16 | 16 |
|
17 | 17 | --- |
18 | 18 |
|
19 | 19 | ## Features |
20 | 20 |
|
21 | | -### Protocol Support |
| 21 | +### Protocol |
22 | 22 |
|
23 | | -- **HTTP/1.0 and HTTP/1.1** — chunked transfer encoding, keep-alive, pipelining |
24 | | -- **HTTP/2** — binary framing, stream multiplexing, HPACK header compression, flow control |
25 | | -- **HTTP/3 (QUIC)** — UDP-based transport, QPACK header compression, 0-RTT connection establishment |
| 23 | +- **HTTP/1.0 and HTTP/1.1** — chunked transfer, keep-alive, pipelining, h2c upgrade detection |
| 24 | +- **HTTP/2** — binary framing, stream multiplexing, HPACK compression, per-stream flow control |
| 25 | +- **HTTP/3 (QUIC)** — UDP transport, QPACK compression, 0-RTT connection establishment |
| 26 | +- **Dynamic protocol negotiation** — ALPN and HTTP/2 preface detection for automatic version selection |
26 | 27 |
|
27 | | -### Resilience |
| 28 | +### Client |
28 | 29 |
|
29 | | -- **Immortal pipeline** — transport failures, protocol violations, and corrupt data are absorbed gracefully. The stream only completes when you dispose the client. No single bad request or broken connection can take down the pipeline. |
30 | | -- **Automatic retries** — idempotent methods (GET, PUT, DELETE, HEAD, OPTIONS) are retried automatically on transient failures. Respects `Retry-After` headers. POST and other non-idempotent methods are never retried. |
31 | | -- **Connection pooling** — per-host connection pools with configurable limits, idle eviction, and automatic reconnect with exponential backoff. Connections are reused transparently across requests. |
| 30 | +- **Immortal pipeline** — transport failures, protocol violations, and corrupt data are absorbed gracefully; the stream only completes when you dispose the client |
| 31 | +- **Automatic retries** — idempotent methods retried on transient failures with `Retry-After` support |
| 32 | +- **Connection pooling** — per-host pools with configurable limits, idle eviction, and exponential backoff reconnect |
| 33 | +- **Redirect following** — 301/302/303/307/308 with correct method rewriting, body preservation, loop detection, and HTTPS downgrade protection |
| 34 | +- **Cookie management** — automatic storage and injection with domain/path matching, `Secure`, `HttpOnly`, `SameSite` support; pluggable via `ICookieJar` |
| 35 | +- **HTTP caching** — LRU cache with `Vary`, conditional requests (`ETag`, `Last-Modified`), freshness evaluation, and 304 merging; pluggable via `ICacheStore` |
| 36 | +- **Content encoding** — automatic gzip, deflate, and Brotli decompression; optional request compression |
| 37 | +- **100-Continue** — `Expect: 100-continue` handling for large request bodies |
| 38 | +- **Alt-Svc** — alternative service discovery and connection migration |
32 | 39 |
|
33 | | -### HTTP Features |
| 40 | +### Server |
34 | 41 |
|
35 | | -- **Redirect following** — 301, 302, 303, 307, 308 with correct method rewriting (POST to GET on 303), body preservation on 307/308, loop detection, and HTTPS-to-HTTP downgrade protection. Configurable max redirects. |
36 | | -- **Cookie management** — automatic cookie storage and injection across requests. Supports domain/path matching, `Secure`, `HttpOnly`, `SameSite`, `Max-Age`, and `Expires`. Bring your own `ICookieJar` implementation or use the built-in `CookieJar`. |
37 | | -- **HTTP caching** — in-memory LRU cache with `Vary` support, conditional requests via `ETag`/`If-None-Match` and `Last-Modified`/`If-Modified-Since`, freshness evaluation (`max-age`, `s-maxage`, `Expires`, heuristic), and 304 response merging. Pluggable via `ICacheStore` for custom storage backends (Redis, disk, etc.). |
38 | | -- **Content encoding** — automatic gzip, deflate, and Brotli response decompression. Optional request body compression. Can be disabled per-client if you need raw compressed bytes. |
39 | | -- **100-Continue** — `Expect: 100-continue` handling for large request bodies. |
| 42 | +- **Standalone HTTP server** — no Kestrel dependency, built entirely on Akka.Streams |
| 43 | +- **ASP.NET-style middleware pipeline** — composable `TurboRequestDelegate` middleware with `Use`, `Map`, and `Run` |
| 44 | +- **Entity gateway** — route HTTP requests to Akka.NET actors with ask/tell semantics, response mapping, and timeout support |
| 45 | +- **Routing and model binding** — attribute-based and fluent route registration with JSON body binding, query string binding, and parameter validation |
| 46 | +- **TLS/HTTPS** — SNI-based certificate selection, client certificate modes (require/allow/deny), renegotiation, and `ITlsHandshakeFeature` |
| 47 | +- **Connection management** — `MaxConcurrentConnections` per listener, connection logging with wire-level hex dumps |
| 48 | +- **Per-protocol server options** — separate `Http1ServerOptions`, `Http2ServerOptions`, `Http3ServerOptions` with RFC-aligned defaults |
40 | 49 |
|
41 | 50 | ### Performance |
42 | 51 |
|
43 | | -- **Zero-allocation internals** — `MemoryPool<byte>`, `Span<T>`, `ReadOnlyMemory<byte>`, and `System.Threading.Channels` throughout the hot path |
44 | | -- **HTTP/2 multiplexing** — multiple concurrent requests over a single TCP connection with header compression and per-stream flow control |
45 | | -- **Backpressure** — Akka.Streams backpressure propagates end-to-end from the network to the caller, preventing buffer bloat and memory exhaustion under load |
46 | | -- **Channel-based API** — for high-throughput scenarios, bypass `SendAsync` and write/read directly to `System.Threading.Channels` for pipelined I/O |
| 52 | +- **Zero-allocation hot paths** — `MemoryPool<byte>`, `Span<T>`, `ReadOnlyMemory<byte>`, and `System.Threading.Channels` throughout |
| 53 | +- **HTTP/2 multiplexing** — multiple concurrent requests over a single TCP connection with per-stream flow control |
| 54 | +- **Backpressure** — Akka.Streams backpressure propagates end-to-end from network to caller |
| 55 | +- **Channel-based API** — bypass `SendAsync` and write/read directly to `System.Threading.Channels` for pipelined I/O |
47 | 56 |
|
48 | 57 | ### Extensibility |
49 | 58 |
|
50 | | -- **Handler pipeline** — compose custom request/response transforms via `TurboHandler` subclasses or inline delegates, ordered FIFO |
51 | | -- **Pluggable storage** — bring your own `ICookieJar` for custom cookie persistence or `ICacheStore` for external cache backends (Redis, disk, etc.) |
52 | | -- **Distributed tracing** — built-in OpenTelemetry-compatible tracing via `TracingBidiStage` for request/response lifecycle visibility |
53 | | -- **DI integration** — first-class `IServiceCollection` support with named and typed clients, `IOptionsMonitor` for runtime configuration changes |
54 | | -- **6,300+ tests** — unit tests, stream stage tests, acceptance tests, integration tests, API tests, and benchmarks |
| 59 | +- **Handler pipeline** — custom request/response transforms via `TurboHandler` subclasses or inline delegates |
| 60 | +- **Pluggable storage** — bring your own `ICookieJar` or `ICacheStore` for custom persistence backends |
| 61 | +- **OpenTelemetry tracing** — built-in `TracingBidiStage` for request/response lifecycle visibility |
| 62 | +- **DI integration** — `IServiceCollection` support with named/typed clients and `IOptionsMonitor` for runtime config changes |
| 63 | +- **Comprehensive test suite** — unit, stream stage, acceptance, integration, API surface, and benchmark tests |
55 | 64 |
|
56 | 65 | --- |
57 | 66 |
|
58 | 67 | ## Getting Started |
59 | 68 |
|
60 | | -### Installation |
61 | | - |
62 | 69 | ```bash |
63 | 70 | dotnet add package TurboHTTP |
64 | 71 | ``` |
65 | 72 |
|
66 | 73 | Requires **.NET 10.0** or later. |
67 | 74 |
|
68 | | -### Basic Usage |
69 | | - |
70 | | -Register and inject via dependency injection: |
| 75 | +### Client |
71 | 76 |
|
72 | 77 | ```csharp |
73 | | -using TurboHTTP; |
74 | | -using System.Net.Http; |
75 | | -using Microsoft.Extensions.DependencyInjection; |
76 | | - |
77 | 78 | var services = new ServiceCollection(); |
78 | | -services.AddTurboHttpClient(options => |
| 79 | +services.AddTurboHttpClient("GitHub", options => |
79 | 80 | { |
80 | | - options.BaseAddress = new Uri("https://api.example.com"); |
| 81 | + options.BaseAddress = new Uri("https://api.github.com"); |
81 | 82 | options.DefaultRequestVersion = HttpVersion.Version20; |
82 | | -}); |
| 83 | +}) |
| 84 | +.WithRedirect() |
| 85 | +.WithCookies() |
| 86 | +.WithRetry(retry => retry.MaxRetries = 3); |
83 | 87 |
|
84 | 88 | var provider = services.BuildServiceProvider(); |
85 | | -var factory = provider.GetRequiredService<ITurboHttpClientFactory>(); |
86 | | -var client = factory.CreateClient(); |
87 | | - |
88 | | -var request = new HttpRequestMessage(HttpMethod.Get, "/users"); |
89 | | -var response = await client.SendAsync(request); |
90 | | - |
91 | | -Console.WriteLine($"Status: {response.StatusCode}"); |
92 | | -var body = await response.Content.ReadAsStringAsync(); |
93 | | -Console.WriteLine(body); |
94 | | -``` |
95 | | - |
96 | | -### Dependency Injection |
97 | | - |
98 | | -Register named or typed clients with `IServiceCollection`: |
99 | | - |
100 | | -```csharp |
101 | | -services |
102 | | - .AddTurboHttpClient("GitHub", options => |
103 | | - { |
104 | | - options.BaseAddress = new Uri("https://api.github.com"); |
105 | | - options.ConnectTimeout = TimeSpan.FromSeconds(15); |
106 | | - options.IdleTimeout = TimeSpan.FromSeconds(30); |
107 | | - }) |
108 | | - .WithRedirect() |
109 | | - .WithCookies() |
110 | | - .WithDecompression() |
111 | | - .WithRetry(retry => retry.MaxRetries = 3) |
112 | | - .WithCache(cache => cache.MaxEntries = 1000); |
113 | | -``` |
114 | | - |
115 | | -Then inject and use: |
116 | | - |
117 | | -```csharp |
118 | | -public class GitHubService(ITurboHttpClientFactory factory) |
119 | | -{ |
120 | | - private readonly ITurboHttpClient _client = factory.CreateClient("GitHub"); |
| 89 | +var client = provider.GetRequiredService<ITurboHttpClientFactory>().CreateClient("GitHub"); |
121 | 90 |
|
122 | | - public async Task<string> GetRepoAsync(string owner, string repo, CancellationToken ct) |
123 | | - { |
124 | | - var request = new HttpRequestMessage(HttpMethod.Get, $"/repos/{owner}/{repo}"); |
125 | | - var response = await _client.SendAsync(request, ct); |
126 | | - return await response.Content.ReadAsStringAsync(ct); |
127 | | - } |
128 | | -} |
| 91 | +var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/users")); |
| 92 | +Console.WriteLine(await response.Content.ReadAsStringAsync()); |
129 | 93 | ``` |
130 | 94 |
|
131 | | -### Channel-based API |
132 | | - |
133 | | -For high-throughput scenarios, bypass `SendAsync` and use channels directly: |
| 95 | +### Server |
134 | 96 |
|
135 | 97 | ```csharp |
136 | 98 | var services = new ServiceCollection(); |
137 | | -services.AddTurboHttpClient(options => |
| 99 | +services.AddTurboServer(server => |
138 | 100 | { |
139 | | - options.BaseAddress = new Uri("https://api.example.com"); |
| 101 | + server.Listen("https://localhost:5001", listen => |
| 102 | + { |
| 103 | + listen.UseHttps(); |
| 104 | + listen.Protocols = HttpProtocols.Http1AndHttp2; |
| 105 | + }); |
140 | 106 | }); |
141 | 107 |
|
142 | | -var provider = services.BuildServiceProvider(); |
143 | | -var factory = provider.GetRequiredService<ITurboHttpClientFactory>(); |
144 | | -var client = factory.CreateClient(); |
145 | | - |
146 | | -// Fire requests |
147 | | -await client.Requests.WriteAsync(new HttpRequestMessage(HttpMethod.Get, "/ping")); |
148 | | -await client.Requests.WriteAsync(new HttpRequestMessage(HttpMethod.Get, "/health")); |
149 | | - |
150 | | -// Read responses as they arrive |
151 | | -await foreach (var response in client.Responses.ReadAllAsync()) |
| 108 | +services.AddTurboRouting(routes => |
152 | 109 | { |
153 | | - Console.WriteLine($"{response.RequestMessage!.RequestUri} -> {response.StatusCode}"); |
154 | | -} |
155 | | -``` |
156 | | - |
157 | | -### Custom Cookie Jar |
158 | | - |
159 | | -Implement `ICookieJar` to plug in your own cookie storage (e.g. encrypted, persistent, or shared across clients): |
160 | | - |
161 | | -```csharp |
162 | | -public sealed class PersistentCookieJar : ICookieJar |
163 | | -{ |
164 | | - public void ProcessResponse(Uri requestUri, HttpResponseMessage response) |
165 | | - { |
166 | | - // parse Set-Cookie headers and persist to your backing store |
167 | | - } |
168 | | - |
169 | | - public void AddCookiesToRequest(Uri requestUri, ref HttpRequestMessage request) |
170 | | - { |
171 | | - // load cookies from your backing store and add Cookie header |
172 | | - } |
173 | | -} |
174 | | - |
175 | | -services |
176 | | - .AddTurboHttpClient("MyApi", options => { ... }) |
177 | | - .WithCookies(new PersistentCookieJar()); |
| 110 | + routes.MapGet("/hello", () => Results.Ok("Hello from TurboHTTP!")); |
| 111 | + routes.MapTurboEntity<OrderActor>("/orders/{id}") |
| 112 | + .Ask(HttpMethod.Get, msg => new GetOrder(msg.RouteValues["id"])) |
| 113 | + .Tell(HttpMethod.Post, msg => new CreateOrder(msg.Body)); |
| 114 | +}); |
178 | 115 | ``` |
179 | 116 |
|
180 | | -### Custom Cache Store |
181 | | - |
182 | | -Implement `ICacheStore` to use Redis, disk, or any other backend instead of the built-in in-memory LRU cache: |
| 117 | +For more examples — channel API, custom handlers, cookie jars, cache stores, entity gateway patterns — see the [documentation site](https://turbohttp.leberkas.org/). |
183 | 118 |
|
184 | | -```csharp |
185 | | -public sealed class RedisCacheStore : ICacheStore |
186 | | -{ |
187 | | - public ICacheEntry? Get(HttpRequestMessage request) { /* Redis lookup */ } |
188 | | - public void Put(HttpRequestMessage request, HttpResponseMessage response, |
189 | | - IMemoryOwner<byte> bodyOwner, int bodyLength, |
190 | | - DateTimeOffset requestTime, DateTimeOffset responseTime) { /* Redis store */ } |
191 | | - public void Invalidate(Uri uri) { /* Redis delete */ } |
192 | | -} |
193 | | - |
194 | | -services |
195 | | - .AddTurboHttpClient("MyApi", options => { ... }) |
196 | | - .WithCache(new RedisCacheStore(), cache => cache.MaxBodyBytes = 10_485_760); |
197 | | -``` |
| 119 | +--- |
198 | 120 |
|
199 | | -### Custom Handlers |
| 121 | +## Architecture |
200 | 122 |
|
201 | | -Extend the pipeline with custom request/response transforms: |
| 123 | +### Client |
202 | 124 |
|
203 | | -```csharp |
204 | | -public sealed class AuthHandler : TurboHandler |
205 | | -{ |
206 | | - public override HttpRequestMessage ProcessRequest(HttpRequestMessage request) |
207 | | - { |
208 | | - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "my-token"); |
209 | | - return request; |
210 | | - } |
211 | | -} |
212 | | - |
213 | | -services |
214 | | - .AddTurboHttpClient("MyApi", options => { ... }) |
215 | | - .AddHandler<AuthHandler>(); |
216 | 125 | ``` |
217 | | - |
218 | | -Or use inline delegates: |
219 | | - |
220 | | -```csharp |
221 | | -services |
222 | | - .AddTurboHttpClient("MyApi") |
223 | | - .UseRequest(request => |
224 | | - { |
225 | | - request.Headers.Add("X-Request-Id", Guid.NewGuid().ToString()); |
226 | | - return request; |
227 | | - }); |
| 126 | +ITurboHttpClient (SendAsync / channel API) |
| 127 | + | |
| 128 | +Feature Pipeline Tracing > Handlers > Redirect > Cookie > Retry > |
| 129 | + Expect-Continue > Cache > ContentEncoding > Alt-Svc |
| 130 | + | |
| 131 | +Engine Version router > per-version client engines |
| 132 | + HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 |
| 133 | + | |
| 134 | +Protocol Encoding/decoding, HPACK/QPACK, frame types |
| 135 | + | |
| 136 | +Transport TCP (ConnectionManagerActor > Channel<byte> > ClientByteMover) |
| 137 | + QUIC (ConnectionManagerActor > QUIC streams) |
228 | 138 | ``` |
229 | 139 |
|
230 | | -### Configuration Options |
231 | | - |
232 | | -| Option | Default | Description | |
233 | | -|--------|---------|-------------| |
234 | | -| `BaseAddress` | `null` | Base URI for resolving relative request URIs | |
235 | | -| `ConnectTimeout` | 10s | Timeout for establishing a new TCP connection | |
236 | | -| `IdleTimeout` | 10s | Time a connection may remain idle before eviction | |
237 | | -| `ReconnectInterval` | 5s | Delay between reconnection attempts after failure | |
238 | | -| `MaxReconnectAttempts` | 10 | Max reconnection attempts before giving up | |
239 | | -| `MaxFrameSize` | 128 KiB | HTTP/2 maximum frame size in bytes | |
240 | | -| `ConnectionPolicy` | `null` | Per-host connection limits and HTTP/2 multiplexing settings | |
241 | | -| `DangerousAcceptAnyServerCertificate` | `false` | Skip TLS validation (dev/test only) | |
242 | | -| `ServerCertificateValidationCallback` | — | Custom TLS certificate validation logic | |
243 | | -| `ClientCertificates` | `null` | X.509 client certificates for mTLS | |
244 | | -| `EnabledSslProtocols` | `SslProtocols.None` | TLS protocol versions to enable (OS default if `None`) | |
245 | | - |
246 | | ---- |
247 | | - |
248 | | -## Architecture |
| 140 | +### Server |
249 | 141 |
|
250 | 142 | ``` |
251 | | -Client Layer ITurboHttpClient (SendAsync / channel API) |
252 | | - ↓ |
253 | | -Feature Layer Akka.Streams BidiStages — outermost to innermost: |
254 | | - Tracing → Handlers → Redirect → Cookie → Retry → |
255 | | - Expect-Continue → Cache → ContentEncoding → Alt-Svc |
256 | | - ↓ |
257 | | -Engine Layer Engine (version router) → per-version engines |
258 | | - Each engine: unified ConnectionStage + NetworkBufferBatchStage |
259 | | - HTTP/1.0 · HTTP/1.1 · HTTP/2 · HTTP/3 |
260 | | - ↓ |
261 | | -Protocol Layer Encoding/decoding, HPACK/QPACK, frame types — all internal |
262 | | - to the unified ConnectionStage per version |
263 | | - ↓ |
264 | | -Transport Layer TcpConnectionStage / QuicConnectionStage |
265 | | - ├─ TCP → ConnectionManagerActor → Channel<byte> → ClientByteMover |
266 | | - └─ QUIC → ConnectionManagerActor → QUIC streams |
| 143 | +Transport TcpListenerStage / QuicListenerStage |
| 144 | + | |
| 145 | +Connection ConnectionActor > protocol negotiation (ALPN / preface detection) |
| 146 | + | |
| 147 | +Protocol Per-version server engines |
| 148 | + HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 |
| 149 | + | |
| 150 | +Context TurboHttpContext (request, response, features, connection info) |
| 151 | + | |
| 152 | +Middleware Pipeline stages: logging > routing > entity dispatch > handlers |
| 153 | + | |
| 154 | +Application TurboRequestDelegate / Actor entity gateway (ask/tell) |
267 | 155 | ``` |
268 | 156 |
|
269 | | -For interactive architecture diagrams, see the [documentation site](https://turbohttp.st0o0.net/). |
270 | | - |
271 | | ---- |
272 | | - |
273 | | -## Documentation |
274 | | - |
275 | | -Full documentation — including feature guides, architecture deep-dives, and a comparison with `HttpClient` — is available at **[https://turbohttp.st0o0.net/](https://turbohttp.st0o0.net/)**. |
| 157 | +For interactive architecture diagrams, see the [documentation site](https://turbohttp.leberkas.org/). |
276 | 158 |
|
277 | 159 | --- |
278 | 160 |
|
279 | 161 | ## Building from Source |
280 | 162 |
|
281 | 163 | ```bash |
282 | | -# Restore and build |
283 | 164 | dotnet restore ./src/TurboHTTP.slnx |
284 | 165 | dotnet build --configuration Release ./src/TurboHTTP.slnx |
285 | 166 |
|
286 | | -# Run tests by project |
287 | | -dotnet test --project ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj # unit |
288 | | -dotnet test --project ./src/TurboHTTP.StreamTests/TurboHTTP.StreamTests.csproj # stream stages |
289 | | -dotnet test --project ./src/TurboHTTP.AcceptanceTests/TurboHTTP.AcceptanceTests.csproj # acceptance |
290 | | -dotnet test --project ./src/TurboHTTP.IntegrationTests/TurboHTTP.IntegrationTests.csproj # integration (network) |
291 | | -dotnet test --project ./src/TurboHTTP.API.Tests/TurboHTTP.API.Tests.csproj # public API surface |
| 167 | +# Tests (xUnit v3 — use dotnet run, not dotnet test) |
| 168 | +dotnet run --project ./src/TurboHTTP.Tests/TurboHTTP.Tests.csproj |
| 169 | +dotnet run --project ./src/TurboHTTP.IntegrationTests/TurboHTTP.IntegrationTests.csproj |
| 170 | +dotnet run --project ./src/TurboHTTP.AcceptanceTests/TurboHTTP.AcceptanceTests.csproj |
292 | 171 |
|
293 | | -# Run benchmarks |
| 172 | +# Benchmarks |
294 | 173 | dotnet run --configuration Release --project ./src/TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj |
295 | 174 | ``` |
296 | 175 |
|
297 | 176 | --- |
298 | 177 |
|
| 178 | +## Documentation |
| 179 | + |
| 180 | +Full documentation — including feature guides, architecture deep-dives, and API references — is available at **[turbohttp.st0o0.net](https://turbohttp.leberkas.org/)**. |
| 181 | + |
| 182 | +--- |
| 183 | + |
299 | 184 | ## Contributing |
300 | 185 |
|
301 | | -Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for branch naming conventions, PR requirements, how to run tests locally, and recommended branch protection settings. |
| 186 | +Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for branch naming conventions, PR requirements, and how to run tests locally. |
302 | 187 |
|
303 | 188 | --- |
304 | 189 |
|
|
0 commit comments