CosmoHttpClient 5.1.0
See the version list below for details.
dotnet add package CosmoHttpClient --version 5.1.0
NuGet\Install-Package CosmoHttpClient -Version 5.1.0
<PackageReference Include="CosmoHttpClient" Version="5.1.0" />
<PackageVersion Include="CosmoHttpClient" Version="5.1.0" />
<PackageReference Include="CosmoHttpClient" />
paket add CosmoHttpClient --version 5.1.0
#r "nuget: CosmoHttpClient, 5.1.0"
#:package CosmoHttpClient@5.1.0
#addin nuget:?package=CosmoHttpClient&version=5.1.0
#tool nuget:?package=CosmoHttpClient&version=5.1.0
CosmoHttpClient
CosmoHttpClient is a .NET 10 HTTP client library built clean-sheet to outperform System.Net.Http.HttpClient while still offering the convenience layer (cookies, redirects, retries, response cache, decompression, NDJSON, telemetry) people expect from an everyday client.
Recommended surface: FastFeatureClient (handler pipeline) over FastAutoClient (auto-negotiates HTTP/1.1, HTTP/2, and HTTP/3 per origin).
using CosmoHttpClient.Fast.Auto;
using CosmoHttpClient.Fast.Features;
await using var transport = new FastAutoClient();
await using var client = new FastFeatureClient(new FastFeatureClientOptions
{
Transport = transport,
Handlers = new IFastFeatureHandler[]
{
new TelemetryHandler(new ActivitySourceTelemetry()),
new ResponseCacheHandler(),
new DecompressionHandler(),
new CookieHandler(),
new RedirectHandler(),
new RetryHandler(),
},
});
await using var resp = await client.GetAsync(new Uri("https://api.example.com/v1/things"));
That client picks H/1.1 vs H/2 via TLS ALPN on the first request to each origin, learns about H/3 from the response's Alt-Svc header, and falls back gracefully when QUIC is unavailable. Pay-as-you-go on every handler — empty handler list = bare-transport performance.
The four transport clients (FastClient, FastH2Client, FastH3Client, FastAutoClient) all implement IFastTransport and remain individually usable when you want to pin a protocol (gRPC needs H/2; benchmarks want a single transport).
Also in tree: CosmoWebSocket (RFC 6455), Grpc/* framing primitives, and a tools/PerfGate runner that enforces BDN-measured ratio + allocation rules on every change.
Requirements
- .NET SDK 10.0+
- For HTTP/3: platform QUIC support (
System.Net.Quic/ MsQuic availability)
The library targets net10.0.
Repository layout
| Path | Purpose |
|---|---|
src/CosmoHttpClient/Fast/ |
FastClient + HTTP/1.1 connection pool, span-based H1 codec |
src/CosmoHttpClient/Fast/Http2/ |
FastH2Client, HPACK, frame layer, multiplexed H/2 connection |
src/CosmoHttpClient/Fast/Http3/ |
FastH3Client, QPACK, QUIC stream pool |
src/CosmoHttpClient/Fast/Auto/ |
FastAutoClient (per-origin protocol cache + ALPN + Alt-Svc), AltSvcParser (RFC 7838) |
src/CosmoHttpClient/Fast/Features/ |
Handler pipeline: FastFeatureClient, Decompression/Cookie/Redirect/Retry/ResponseCache/Telemetry handlers, JSON + NDJSON extensions |
src/CosmoHttpClient/Connections/ |
Socket / TLS / SOCKS5 transport — used by CosmoWebSocket |
src/CosmoHttpClient/WebSockets/ |
CosmoWebSocket (RFC 6455) |
src/CosmoHttpClient/Grpc/ |
gRPC framing primitives |
tests/CosmoHttpClient.Tests |
xUnit suite (~470 tests) |
samples/CosmoHttpClient.Sample |
Runnable sample app |
benchmarks/CosmoHttpClient.Benchmarks |
BDN benchmarks |
benchmarks/perf-targets.json |
Perf-gate rules consumed by tools/PerfGate |
tools/PerfGate |
BDN runner + JSON parser; exits non-zero on regression |
docs/ |
RFCs (P1.10 streaming, P2 H/2, P4 features, P5 auto), retrospectives |
Build, test, run
dotnet build --nologo
dotnet test --nologo --no-build
dotnet run --project samples/CosmoHttpClient.Sample
dotnet run --project samples/CosmoHttpClient.Sample -- --h2 https://www.cloudflare.com/
dotnet run --project samples/CosmoHttpClient.Sample -- --h3 https://www.cloudflare.com/
Run the perf gate (full BDN sweep + rule validation, ~15-20 min):
dotnet run -c Release --project tools/PerfGate
# Or evaluate against existing artifacts (no BDN re-run):
dotnet run -c Release --project tools/PerfGate -- --results gate-artifacts/results
How auto-negotiation works
FastAutoClient holds one each of FastClient, FastH2Client, and (lazily) FastH3Client, plus a per-origin (scheme, host, port) → CachedProtocol map.
- First request to a new https:// origin routes through
FastH2Client. Its TLS handshake advertises ALPN[h2, http/1.1]; whatever the server picks gets cached. If the server picks H/1.1, the dispatcher catchesNotSupportedExceptionand demotes the request toFastClientfor the same call. - Every successful https:// response has its
Alt-Svcheader parsed (RFC 7838). On anh3advertisement targeting the same origin port, the cache flips toHttp3for subsequent requests with TTL =min(MaxAltSvcLifetime, ma). - Cached H/3 dispatch wraps the H/3 call in a
try/catchforQuicException/IOException/SocketException/TimeoutException— bumps aFailureCount. AfterDemotionThreshold(default 3) consecutive failures, the entry sticky-demotes back to H/2 forMaxAltSvcLifetime(andAlt-Svc h3re-upgrades are suppressed during that window so we don't oscillate). - Plain http:// always picks H/1.1 (no h2c upgrade probe). Pin via
PreferredProtocol = HttpVersion.Version20if you need h2c against a server you trust to support it.
Knobs on FastAutoClientOptions:
| Option | Meaning |
|---|---|
PreferredProtocol |
Pin every request to one protocol (Version11/20/30); skips ALPN+Alt-Svc entirely. |
DisabledProtocol |
Never use this protocol even if advertised. Good for Version30 in environments that block UDP. |
MaxAltSvcLifetime |
Cap on Alt-Svc-driven cache TTL + sticky-demote duration. Default 24h. |
DemotionThreshold |
Consecutive H/3 failures before sticky demote. Default 3. |
Http11ClientOptions / Http2ClientOptions / Http3ClientOptions |
Configure the inner sub-clients. |
When to pin a transport
Use the explicit per-protocol clients (instead of FastAutoClient) when:
- gRPC — pin to
FastH2Client. gRPC requires H/2 framing; auto-negotiating away from it would break. - Single-protocol benchmarks —
tools/PerfGaterules use the explicit clients to isolate one transport at a time. - Forcing H/3 — for an origin you know speaks QUIC,
FastH3Clientskips the H/2 probe + Alt-Svc upgrade hop entirely. - Forcing H/1.1 on https:// — if you've measured that the H/2 negotiation adds latency you don't recover, pin to
FastClient.
All four transport clients implement IFastTransport, so they all plug into FastFeatureClient the same way:
new FastFeatureClient(new FastFeatureClientOptions { Transport = new FastH2Client() });
Quick start — FastClient (HTTP/1.1)
using CosmoHttpClient.Fast;
await using var client = new FastClient();
await using var resp = await client.GetAsync(new Uri("https://api.example.com/v1/things"));
Console.WriteLine($"{resp.StatusCode}: {Encoding.UTF8.GetString(resp.Body.Span)}");
POST with JSON body and headers:
private static readonly HeaderPair[] AuthHeaders = {
new("User-Agent", "myapp/1.0"),
new("Content-Type", "application/json"),
new("Authorization", $"Bearer {token}"),
};
var body = JsonSerializer.SerializeToUtf8Bytes(new { name = "widget" });
await using var resp = await client.PostAsync(uri, body, AuthHeaders);
Streaming a large response via PipeReader (Content-Length and chunked both supported):
await using var resp = await client.GetStreamAsync(new Uri("https://example.com/big.bin"));
var rdr = resp.BodyReader;
while (true)
{
var read = await rdr.ReadAsync();
foreach (var seg in read.Buffer) await output.WriteAsync(seg);
rdr.AdvanceTo(read.Buffer.End);
if (read.IsCompleted) break;
}
Lifetime contract: a streaming response borrows a pool connection until disposed. Drain-then-dispose returns the connection; dispose-without-drain marks the connection broken (wire state is unknown) and the pool drops it. Set FastClientOptions.BackgroundDrainOnDispose = true to opt into a fire-and-forget drain that recovers the connection at the cost of a background task per abandoned response.
FastClient exposes GetAsync, GetStreamAsync, PostAsync, and SendAsync / SendStreamAsync(method, uri, body, headers, ct) for arbitrary verbs. Buffered response body is ReadOnlyMemory<byte> aliasing the connection's arena, valid until the response is disposed. Headers iterate via a zero-allocation Http1HeaderEnumerator.
Quick start — FastH2Client (HTTP/2)
using CosmoHttpClient.Fast.Http2;
await using var h2 = new FastH2Client();
await using var res = await h2.GetAsync(new Uri("https://www.cloudflare.com/"));
Console.WriteLine($"H/2 {res.StatusCode}, {res.Body.Length} bytes");
One multiplexed connection per origin (h2c plain or h2 over TLS+ALPN), span-based HPACK + frame layer, friendly GetAsync / PostAsync. Custom request headers via the (uri, headers, ct) overload — names are validated + lowercased per RFC 7540 §8.1.2 (hop-by-hop and pseudo-headers throw before any frame goes on the wire).
Quick start — FastH3Client (HTTP/3)
using CosmoHttpClient.Fast.Http3;
await using var h3 = new FastH3Client();
try
{
await using var res = await h3.GetAsync(new Uri("https://www.cloudflare.com/"));
Console.WriteLine($"H/3 {res.StatusCode}, {res.Body.Length} bytes");
}
catch (FastH3NotSupportedException) { /* QUIC not available */ }
QUIC stream multiplexing, static + dynamic QPACK. Throws FastH3NotSupportedException on platforms without QUIC.
Handler pipeline reference
FastFeatureClient walks handlers in declared order — first runs outermost. Recommended layering:
Telemetry → ResponseCache → Decompression → Cookies → Redirects → Retries → terminal transport
Why this order:
- Telemetry outermost so spans cover everything, including retries / redirects.
- ResponseCache outside Decompression so the cache stores wire bytes (so the same entry serves clients with different
Accept-Encoding). - Decompression outside Cookies / Redirects so subsequent handlers see plain bodies.
- Retries innermost (around the transport) so a retried request does not loop the redirect / cookie machinery.
Deviate when you have a reason — the framework just hands you control.
JSON / NDJSON helpers compose on top of the same client:
var widget = await client.GetJsonAsync(uri, MyJsonContext.Default.Widget);
await foreach (var line in client.GetNdjsonAsync(uri, MyJsonContext.Default.LogLine, ct))
Process(line);
CosmoWebSocket
using CosmoHttpClient.WebSockets;
await using var ws = await CosmoWebSocket.ConnectAsync("wss://example.com/socket");
await ws.SendAsync(payload, CosmoWebSocketMessageType.Binary, endOfMessage: true, ct);
Optional permessage-deflate, ping/pong keepalive, RFC 6455 close handshake. Built on the same Connections/ socket + TLS + SOCKS5 plumbing as the rest of the library.
Performance
Latest BDN sweep on Apple M1 / .NET 10 / Kestrel loopback. Cosmo / HttpClient ratios — values < 1.00 mean Cosmo is faster.
| Benchmark | Cosmo | HttpClient | Ratio |
|---|---|---|---|
| Data plane (write+parse, in-memory) | 151 ns / 0 B | n/a (vs naive) | 0.74× of naive baseline |
| H1.1 single connection (1 KiB GET) | 47.2 µs / 771 B | 51.4 µs / 3.4 KB | 0.92× / 4.4× less alloc |
| H1.1 pooled, single-threaded | 42.9 µs / 946 B | 51.7 µs / 3.4 KB | 0.83× / 3.6× less alloc |
| H1.1 pooled, 8 workers × 64 reqs | 4.93 ms / 187 KB | 4.88 ms / 1.6 MB | 1.01× / 8.7× less alloc |
Friendly FastClient API (1 KiB GET) |
43.4 µs / 1382 B | 51.1 µs / 3.4 KB | 0.85× / 2.5× less alloc |
Friendly FastClient over HTTPS |
55.3 µs / 3022 B | 63.6 µs / 4.95 KB | 0.87× / 1.7× less alloc |
Friendly FastClient streaming 4 MiB |
1.08 ms / 101 KB | 1.09 ms / 100 KB | 0.87× / parity |
The architectural promise — 0 alloc/op on the data plane in steady state — is asserted by FastH1AllocDiagnosticTests on every test run, independent of BDN's amortization noise.
A separate Windows VM baseline (informational, not a gate) lives in docs/PERF-WINDOWS-BASELINE.md.
Releasing
Releases publish via .github/workflows/publish.yml on any pushed tag matching v*.
git tag v5.0.0
git push origin v5.0.0
The workflow strips the leading v, builds Release, runs the full test suite, packs CosmoHttpClient.<version>.nupkg + .snupkg, and pushes both to nuget.org via --skip-duplicate. The .snupkg carries SourceLink metadata so consumers can step into the library source from their debugger.
Configure once on GitHub: Settings → Secrets and variables → Actions → New repository secret named NUGET_API_KEY with a nuget.org API key scoped to CosmoHttpClient.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Cosmo.Transport (>= 1.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- ZstdSharp.Port (>= 0.8.4)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on CosmoHttpClient:
| Package | Downloads |
|---|---|
|
CosmoBlob.Client
CosmoHttpClient-backed client for CosmoKvD's /blobs/{key} surface. Drop into any .NET 10 app that wants an opaque blob store; pair with a thin IBlobStore adapter when used with CosmoMailServer. v1.0.0 swaps System.Net.Http.HttpClient for CosmoHttpClient's FastAutoClient (HTTP/1.1+2+3 auto-negotiation, span-based hot path). |
|
|
CosmoMail
Lightweight .NET SMTP and IMAP client library with MIME generation, templating, attachments, inline images, and STARTTLS support. |
GitHub repositories
This package is not used by any popular GitHub repositories.