CosmoHttpClient 2.0.0
See the version list below for details.
dotnet add package CosmoHttpClient --version 2.0.0
NuGet\Install-Package CosmoHttpClient -Version 2.0.0
<PackageReference Include="CosmoHttpClient" Version="2.0.0" />
<PackageVersion Include="CosmoHttpClient" Version="2.0.0" />
<PackageReference Include="CosmoHttpClient" />
paket add CosmoHttpClient --version 2.0.0
#r "nuget: CosmoHttpClient, 2.0.0"
#:package CosmoHttpClient@2.0.0
#addin nuget:?package=CosmoHttpClient&version=2.0.0
#tool nuget:?package=CosmoHttpClient&version=2.0.0
CosmoHttpClient
CosmoHttpClient is a .NET 10 HTTP client library. The repository contains two surfaces:
CosmoHttpClient.Fast(recommended for new code) — a span-based, arena-buffered HTTP/1.1 + HTTP/2 client built clean-sheet to outperformSystem.Net.Http.HttpClient. Per-core sharded H1 pool, single multiplexed H2 connection per origin, zero per-op allocation steady state, friendlyFastClientandFastH2ClientAPIs. BeatsHttpClienton every measured benchmark (see Performance).- Legacy surface —
CosmoClient,CosmoHttp3Client,CosmoPipelineClient,CosmoWebSocket. Feature-rich (cookies, redirects, retries, response cache, decompression, NDJSON), built onSystem.IO.PipelinesandCosmo.Transport. Stays in tree pre-1.0 while the fast surface grows toward parity.
A tools/PerfGate runner enforces 18 BDN-measured rules on every change; documentation under docs/ covers the P1 retrospective and the P1.10 / P2 RFCs.
What's new (P1: 2026-05)
FastClientpublic API —GetAsync/PostAsync/SendAsyncover plain HTTP and HTTPS, with custom headers, request bodies, and per-(scheme, host, port)connection isolation.- Span-based HTTP/1.1 codec —
Http1Writer+Http1Readerparse and serialize on byte spans with zero allocations on the data plane (test-asserted viaFastH1AllocDiagnosticTests). - Per-core sharded
FastH1ConnectionPool— lock-freeInterlocked.Exchangefast path, cross-shard sweep on miss; 0.90× HttpClient latency, 8.7× less allocation under 8-way concurrent load. - TLS via ALPN —
FastClienttalkshttps://, configurable cert validator, ALPN pinned tohttp/1.1. 0.85× HttpClient latency on the TLS friendly API. - Chunked transfer-encoding response support — two-pass dechunker (scan-then-compact) handles bodies split across socket reads.
tools/PerfGate— runs the BDN suite, parses the JSON, validates 18 rules with explicit ratio + alloc thresholds, exits non-zero on regression.- Three RFCs under
docs/— P1 retrospective, P1.10 streaming-body design + todo list, P2 HTTP/2 rewrite design + todo list (37 items).
What it supports
Core transports
| Surface | Purpose |
|---|---|
FastClient (in CosmoHttpClient.Fast) |
Recommended for new code. HTTP/1.1 over plaintext or TLS. Span-based codec, arena-allocated buffers, per-core sharded pool. 0 alloc/op steady state; beats HttpClient on every benchmark. |
FastH2Client (in CosmoHttpClient.Fast.Http2) |
Recommended for new code. HTTP/2 over h2c or h2 (TLS+ALPN). Single multiplexed connection per origin, span-based HPACK + frame layer, friendly GetAsync/PostAsync API. Beats HttpClient on every measured h2 benchmark. |
CosmoClient |
Legacy HTTP/1.1 client with pooling, retries, redirects, cookies, decompression, caching, and telemetry |
CosmoHttp3Client |
HTTP/3 client over QUIC with TLS resumption support |
CosmoPipelineClient |
HTTP/1.1 pipelining for trusted origins that explicitly support it |
CosmoWebSocket |
RFC 6455 WebSocket client with optional permessage-deflate |
Features
- connection pooling with per-host and global limits
- connection lifetime and idle eviction
- redirects with cross-origin auth stripping
- retries with exponential backoff and
Retry-After - cookie container support, including per-request jars
- gzip, deflate, Brotli, and zstd response decompression
- manual request body compression helpers
- in-memory RFC 7234-style response cache
- HTTP/2 and HTTP/3 server push integration with the cache
- streaming response helpers (
ReadBodyAsBytesAsync,ReadBodyAsStringAsync,ReadBodyAsJsonAsync<T>,ReadBodyAsNdjsonAsync<T>for NDJSON / JSON Lines) - unary gRPC client (wire-compatible with CosmoApiServer)
- OpenTelemetry-style tracing and metrics via
ActivitySourceandMeter - DI helpers for registering default, named, and typed clients
- custom DNS resolver, SOCKS5/proxy, mTLS, and socket tuning hooks
- extension points for socket-level transport (
ICosmoConnectionFactory) and request-level execution (ICosmoRequestExecutor, e.g. iOS NSURLSession background sessions)
Requirements
- .NET SDK 10.0+
- For HTTP/3: platform QUIC support (
System.Net.Quic/ MsQuic availability)
The main library targets net10.0.
Repository layout
| Path | Purpose |
|---|---|
src/CosmoHttpClient |
Library source. The Fast/ subfolder holds the new HTTP/1.1 fast surface. |
tests/CosmoHttpClient.Tests |
xUnit test suite (309 tests, including 65 fast-surface + 1 alloc diagnostic) |
samples/CosmoHttpClient.Sample |
Runnable sample app |
benchmarks/CosmoHttpClient.Benchmarks |
BenchmarkDotNet benchmarks for both legacy and fast surfaces, plus an in-memory data-plane microbench |
benchmarks/perf-targets.json |
18 perf-gate rules (latency ratio + max alloc) consumed by tools/PerfGate |
benchmarks/CosmoHttpClient.Microbench |
Component-level micro-benchmarks |
tools/PerfGate |
Runner that executes BDN with --exporters json, parses the report, validates rules, exits non-zero on regression |
docs/ |
P1 retrospective and P1.10 / P2 RFCs (see Documentation) |
Build, test, and run
dotnet build --nologo
dotnet test --nologo --no-build
Run the sample:
dotnet run --project samples/CosmoHttpClient.Sample
Useful sample modes:
dotnet run --project samples/CosmoHttpClient.Sample -- --matrix
dotnet run --project samples/CosmoHttpClient.Sample -- --issues
dotnet run --project samples/CosmoHttpClient.Sample -- --h2 https://www.cloudflare.com/
dotnet run --project samples/CosmoHttpClient.Sample -- --h3 https://www.cloudflare.com/
dotnet run --project samples/CosmoHttpClient.Sample -- --pipeline
Run benchmarks:
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks
Run the perf gate (full BDN sweep + 18-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
PerfGate exits non-zero on any rule failure. Rules are defined in benchmarks/perf-targets.json.
Performance
Latest BDN sweep on Apple M1 / .NET 10 / Kestrel loopback. Cosmo-fast / HttpClient ratios — values < 1.00 mean the fast surface is faster.
| Benchmark | Cosmo Fast | 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 (PipeReader) |
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 second Streaming_PerOp_Alloc_Should_Be_Bounded diagnostic verifies streaming round-trips stay below 30 KB/op with no Gen2 collections.
See docs/P1-RETROSPECTIVE.md for the full numbers, the bugs caught, and the design decisions.
Documentation
| Document | Purpose |
|---|---|
docs/P1-RETROSPECTIVE.md |
What shipped in CosmoHttpClient.Fast, the measured numbers, the architectural bets, the bugs surfaced and fixed mid-slice, and lessons (BDN per-op alloc isn't steady state, sharded pools need cross-sweep, two-pass parsers, etc.). |
docs/P1.10-STREAMING-RFC.md |
Design + 14-item todo list for incremental response body via PipeReader over the fast surface. The last functional gap. |
docs/P2-HTTP2-RFC.md |
Design + checklist for the HTTP/2 rewrite that landed CosmoHttpClient.Fast.Http2. 8 phases (P2.0-P2.8), each shipped independently. |
Releasing
Releases are published automatically by .github/workflows/publish.yml on any pushed tag matching v*.
# Cut a release for, e.g., 0.2.0
git tag v0.2.0
git push origin v0.2.0
The workflow strips the leading v, builds Release, runs the full test suite, packs CosmoHttpClient.<version>.nupkg and .snupkg, and pushes both to nuget.org via --skip-duplicate (so re-running the same tag is a no-op). The .snupkg carries SourceLink metadata so consumers can step into the library source directly 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 the CosmoHttpClient package.
Quick start — FastClient (recommended)
using CosmoHttpClient.Fast;
await using var client = new FastClient();
await using var resp = await client.GetAsync(new Uri("http://api.example.com/v1/things"));
Console.WriteLine($"{resp.StatusCode}: {Encoding.UTF8.GetString(resp.Body.Span)}");
POST with JSON body and auth header:
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);
HTTPS with custom cert validation:
var options = new FastClientOptions
{
ServerCertificateValidator = (_, cert, chain, errors) => { /* pin / validate */ return true; },
};
await using var client = new FastClient(options);
await using var resp = await client.GetAsync(new Uri("https://api.example.com/"));
Streaming a large response body via PipeReader (Content-Length and chunked, both supported):
await using var resp = await client.GetStreamAsync(new Uri("http://example.com/big.bin"));
Console.WriteLine($"{resp.StatusCode}, content-length={resp.ContentLength}");
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: the streaming response borrows its connection from the pool until disposed. If you fully drain the body and then dispose, the connection returns to the pool. If you dispose before draining, the connection is marked broken (the wire state is unknown) and the pool drops it on the next return. 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.
The FastClient API supports GetAsync, GetStreamAsync, PostAsync, and SendAsync/SendStreamAsync(method, uri, body, headers, ct) for arbitrary HTTP methods (HEAD/PUT/PATCH/DELETE/OPTIONS, plus custom). Buffered response body is ReadOnlyMemory<byte> aliasing the connection's arena — valid until the response is disposed. Headers are exposed via a zero-allocation Http1HeaderEnumerator. See docs/P1-RETROSPECTIVE.md for what's intentionally not in this surface (cookies, redirects, retries, response cache — use the legacy CosmoClient below if you need those).
Quick start — legacy CosmoClient
Basic GET
await using var client = new CosmoClient();
await using var response = await client.GetAsync("https://example.com/");
var body = await response.ReadBodyAsStringAsync();
Console.WriteLine($"{response.StatusCode} {response.ReasonPhrase}");
Console.WriteLine(body);
JSON helpers
await using var client = new CosmoClient();
var model = await client.GetFromJsonAsync<MyDto>("https://api.example.com/items/42");
await using var postResponse = await client.PostAsJsonAsync(
"https://api.example.com/items",
new CreateItemRequest("demo"));
Sending a custom request
var request = new CosmoRequest
{
Method = CosmoHttpMethod.Post
};
request.SetUri("https://api.example.com/upload");
request.Headers.Add("X-Tenant", "alice");
request.SetBytesBody(File.ReadAllBytes("payload.bin"), "application/octet-stream");
await using var client = new CosmoClient();
await using var response = await client.SendAsync(request);
Streaming by default
CosmoResponse.Body is a Stream. The body is not automatically buffered unless you call helper methods such as:
ReadBodyAsBytesAsync()ReadBodyAsStringAsync()ReadBodyAsJsonAsync<T>()ReadBodyAsNdjsonAsync<T>()— streamingIAsyncEnumerable<T>over newline-delimited JSON (paired with CosmoApiServer's NDJSON streaming endpoints)
That keeps sequential reads and large responses cheap when you only need a stream.
await using var client = new CosmoClient();
await using var response = await client.GetAsync("https://example.com/large-file");
await using var file = File.Create("large-file.bin");
await response.Body.CopyToAsync(file);
NDJSON / JSON Lines streaming (one JSON object per line):
await using var resp = await client.GetAsync("https://api.cosmo/log/tail");
await foreach (var entry in resp.ReadBodyAsNdjsonAsync<LogEntry>(ct: ct))
{
Console.WriteLine($"{entry.Timestamp:O} {entry.Level} {entry.Message}");
}
Main public types
CosmoClient
Use CosmoClient when you want HTTP/1.1 with pooling and the full resilience/cookie/decompression stack.
Key members:
CosmoClient.DefaultGetAsyncSendAsyncGetFromJsonAsync<T>PostAsJsonAsync<T>EvictHostAsync
CosmoClient.Default exists for long-lived singleton usage; avoid creating a new client per request.
FastH2Client
FastH2Client (in CosmoHttpClient.Fast.Http2) is the recommended HTTP/2 surface — span-based HPACK, multiplexed streams over a single connection per origin, h2c (http://) and h2 over TLS+ALPN (https://).
await using var client = new FastH2Client();
await using var resp = await client.GetAsync(new Uri("https://www.cloudflare.com/"));
Console.WriteLine($"{resp.StatusCode}: {resp.Body.Length} bytes");
For TLS with a custom certificate validator:
var options = new FastH2ClientOptions
{
ServerCertificateValidator = (_, _, _, _) => true, // dev only
};
await using var client = new FastH2Client(options);
CosmoHttp3Client
Use CosmoHttp3Client for explicit HTTP/3:
- QPACK
- QUIC transport
- TLS session resumption support via
Http3AllowTlsResume - server push handling into the response cache
If the runtime or platform lacks QUIC support, HTTP/3 requests fail with Http3NotSupportedException.
CosmoPipelineClient
CosmoPipelineClient pins to one origin and pipelines requests across a fixed number of HTTP/1.1 connections.
Use it only when:
- the origin is trusted
- the server explicitly supports pipelining
- response-body materialization is acceptable
Pipelined responses are buffered before being returned because HTTP/1.1 pipelining is FIFO.
CosmoWebSocket
CosmoWebSocket exposes RFC 6455 semantics similar to System.Net.WebSockets.WebSocket.
var options = new CosmoWebSocketOptions
{
EnablePerMessageDeflate = true
};
options.SubProtocols.Add("chat");
await using var ws = await CosmoWebSocket.ConnectAsync("wss://example.com/socket", options);
await ws.SendAsync("hello"u8.ToArray(), CosmoWebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
Capabilities:
ws://andwss://- subprotocol negotiation
- custom headers
- keepalive ping/pong
- optional
permessage-deflate
Configuration
Most behavior is controlled through CosmoHttpOptions.
Quick tuning profiles
Use built-in presets for common goals:
// Low p99 latency for interactive APIs
var lowLatency = CosmoHttpTuningProfiles.LowLatency();
// Higher concurrency / throughput
var highThroughput = CosmoHttpTuningProfiles.HighThroughput();
// Safer memory envelope under heavy payloads
var memorySafe = CosmoHttpTuningProfiles.MemorySafe();
// Talking to a CosmoApiServer backend — narrows compression to gzip-only
// (server only does gzip), enables HTTP/2 server-push capture, turns on
// HTTP/2 keepalive PINGs, and bumps connection lifetime.
var cosmoApi = CosmoHttpTuningProfiles.ForCosmoApiServer();
await using var client = new CosmoClient(lowLatency);
Each profile can also mutate an existing options object:
var options = new CosmoHttpOptions { UserAgent = "myapp/1.0" };
CosmoHttpTuningProfiles.MemorySafe(options);
Connection and timeout settings
| Option | Meaning |
|---|---|
MaxConnectionsPerHost |
Per-origin connection cap |
MaxTotalConnections |
Global connection cap across all hosts |
MaxIdleConnectionDuration |
Idle eviction threshold |
MaxConnectionLifetime |
Absolute connection lifetime cap |
MaxConnectionWaitTimeout |
Time to wait for a free pooled connection |
ConnectTimeout |
TCP/TLS/QUIC connect timeout |
RequestTimeout |
End-to-end request timeout |
IdleSweepInterval |
Frequency of idle pool cleanup |
ExpectContinueTimeout |
How long to wait for 100 Continue |
TcpNoDelay |
Enables/disables TCP_NODELAY |
Response handling and resilience
| Option | Meaning |
|---|---|
AutomaticDecompression |
Response decompression flags |
MaxResponseHeadersByteSize |
Hard cap on response header bytes |
MaxResponseContentLength |
Cap for helper-based buffering/materialization paths |
AllowAutoRedirect / MaxAutomaticRedirections |
Redirect behavior |
MaxRetries |
Number of retries |
IsRetryable |
Custom retry predicate |
InitialBackoff, MaxBackoff, BackoffMultiplier, JitterFactor, MaxRetryAfter |
Retry timing controls |
Cookies, auth, proxy, and DNS
| Option | Meaning |
|---|---|
CookieContainer |
Shared cookie jar |
CredentialProvider |
Shared auth provider |
ProxyUri / ProxyCredentials |
Proxy support |
DnsResolver |
Custom DNS resolver |
SocketOptions |
Low-level socket tuning |
ConnectionFactoryOverride |
Full transport override hook |
Protocol-specific settings
| Option | Meaning |
|---|---|
Http2KeepAliveInterval / Http2KeepAliveTimeout |
HTTP/2 ping keepalive |
Http3AllowTlsResume |
Enables TLS 1.3 session resumption for HTTP/3 reconnects |
ResponseCache |
Shared in-memory response cache |
EnableServerPush |
Enables push handling when a cache is configured |
Per-request overrides
CosmoRequest supports request-local behavior:
HeadersCookieContainerCredentialProviderBodyBodyStreamBodyLength
Helper methods include:
SetUriSetTextBodySetBytesBodySetJsonBodySetMultipartBodySetCompressedBodySetBearerTokenSetBasicAuth
Extension points
Two pluggable seams on CosmoHttpOptions let you swap implementations without forking the library:
| Option | Level | When to use |
|---|---|---|
ConnectionFactoryOverride : ICosmoConnectionFactory |
Socket / byte-stream | Custom byte-stream transports (Unix-domain sockets, in-process duplex pipes for tests). Default opens TCP and wraps with SslStream. |
RequestExecutorOverride : ICosmoRequestExecutor |
Whole request | Bypass the per-host TCP pool entirely. Primary use case is iOS background URLSession — see iOS background requests. The executor produces a CosmoResponse; cookies, decompression, redirect, retry, telemetry, and the response cache still wrap the call. |
var options = new CosmoHttpOptions
{
ConnectionFactoryOverride = new MyUnixSocketFactory(),
RequestExecutorOverride = myCustomExecutor,
};
gRPC
The framing primitives (CosmoGrpcFraming, CosmoGrpcResponse<T>, CosmoGrpcStatus, CosmoGrpcException) are wire-compatible with CosmoApiServer's gRPC middleware: 5-byte length prefix + payload, status carried in the grpc-status / percent-encoded grpc-message trailers, te: trailers request header. They stay payload-format-agnostic, so you can plug Google.Protobuf, protobuf-net, MessagePack, or anything else.
The unary-gRPC extension method that previously hung off CosmoHttp2Client was removed when the legacy H2 stack was deleted in P2.8. A FastH2Client-based replacement is on the roadmap; in the meantime, callers can compose FastH2Client.PostAsync with CosmoGrpcFraming directly.
Caching
The repository includes an in-memory RFC 7234-style cache (CosmoResponseCache).
Highlights:
- GET/HEAD caching
- freshness and validator handling
- conditional revalidation with
ETag/Last-Modified only-if-cached- HTTP/3 pushed responses can populate the same cache
Example:
var cache = new CosmoResponseCache();
await using var client = new CosmoClient(new CosmoHttpOptions
{
ResponseCache = cache,
EnableServerPush = true
});
Dependency injection
The DI extensions live under CosmoHttpClient.DependencyInjection.
Register the default client
services.AddCosmoClient(options =>
{
options.UserAgent = "myapp/1.0";
options.MaxRetries = 2;
});
Register a named client
services.AddCosmoClient("github", options =>
{
options.UserAgent = "myapp/1.0";
});
Resolve it through ICosmoClientFactory:
public sealed class GitHubService(ICosmoClientFactory factory)
{
public async Task<string> GetZenAsync()
{
var client = factory.Create("github");
await using var response = await client.GetAsync("https://api.github.com/zen");
return await response.ReadBodyAsStringAsync();
}
}
Register a typed client
services.AddCosmoClient<MyApiClient>(options =>
{
options.UserAgent = "myapp/1.0";
});
The typed client constructor must take CosmoClient as its first argument.
Telemetry
CosmoTelemetry exposes:
ActivitySourceName = "CosmoHttpClient"MeterName = "CosmoHttpClient"
Metrics include:
http.client.request.durationhttp.client.active_requestshttp.client.request.body.sizehttp.client.response.body.size
Trace context headers are injected when an active W3C activity exists and the request has not already set traceparent / tracestate.
Benchmarks
The benchmark project compares CosmoHttpClient (both legacy and fast surfaces) against Microsoft HttpClient over a loopback Kestrel server. Each suite covers four scenarios: GET plaintext 1 KiB, GET JSON, POST echo 4 KiB, download 64 KiB. The fast surface adds streaming, concurrent multiplex, single-connection, pool, friendly-API, and TLS variants.
For headline fast-surface numbers vs
HttpClient, see Performance above. This section retains the legacy-surface comparison for completeness.
BenchmarkDotNet config: 3 warmups + 10 measured iterations ([SimpleJob(warmupCount: 3, iterationCount: 10)]) — confidence intervals are tight enough (Error/Mean typically 1-5%) without 10-minute run times. Apple M1, .NET 10.0.1.
Legacy CosmoClient (HTTP/1.1)
| Scenario | CosmoClient | HttpClient |
|---|---|---|
| GET plaintext 1 KiB | 50.55 us, 4.19 KB | 52.91 us, 3.31 KB |
| GET JSON | 49.99 us, 4.50 KB | 53.70 us, 2.68 KB |
| POST echo 4 KiB | 53.16 us, 3.66 KB | 53.75 us, 3.24 KB |
| Download 64 KiB | 78.72 us, 68.29 KB | 72.86 us, 67.42 KB |
Fast HTTP/2 (FastH2Client)
| Scenario | FastH2Client | HttpClient |
|---|---|---|
| GET plaintext 1 KiB (h2c) | 52.06 us, 1.94 KB | 57.11 us, 4.02 KB |
| GET plaintext 1 KiB (h2 over TLS) | 61.97 us, 3.12 KB | 65.78 us, 5.27 KB |
| 16-stream concurrent GETs | 166.11 us, 28.14 KB | 145.85 us, 64.52 KB |
| 100-stream concurrent GETs | 650.67 us, 172.93 KB | 433.66 us, 401.63 KB |
| HPACK + frame compose/parse (in-memory) | 207.7 ns, 0 B | 300.2 ns, 1.70 KB (string-bag baseline) |
The fast surface is significantly more allocation-efficient than HttpClient across the board (≈40-60% of HttpClient's per-op allocations). Latency is competitive on small payloads and currently slower on the highest-concurrency stress benchmarks; tracking under the fast-h2-* perf gates.
Benchmark reports (markdown + CSV + HTML) are written under BenchmarkDotNet.Artifacts/results/ after each run.
To re-run a single suite:
# Fast-surface benchmarks
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastDataPlaneBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1ConnectionBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1ConnectionPoolBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1PoolConcurrencyBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastClientBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH1TlsBenchmarks*'
# H/2 fast-surface benchmarks
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2DataPlaneBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2ConnectionBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*FastH2ClientBenchmarks*'
# Legacy H/1.1 benchmarks (CosmoClient)
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*Http11ClientBenchmarks*'
# Full sweep + perf-gate validation in one shot
dotnet run -c Release --project tools/PerfGate
Platform notes
iOS background requests
When iOS suspends a backgrounded app, in-process TCP I/O dies with The network connection was lost because the runtime is paused. To keep transfers alive while the app is suspended, the OS-level NSURLSession configured for background mode must own the request lifecycle.
CosmoClient exposes the ICosmoRequestExecutor extension point for this — wire-level transports plug at ICosmoConnectionFactory, but NSURLSession is request-shaped and needs the higher-level seam. Set CosmoHttpOptions.RequestExecutorOverride to dispatch every request through your own executor instead of the per-host TCP pool. Cookies, decompression, redirect, retry, telemetry, and the response cache still wrap the call.
var options = new CosmoHttpOptions
{
#if IOS
RequestExecutorOverride = DeviceInfo.DeviceType == DeviceType.Virtual
? null // iOS Simulator doesn't support background sessions
: new NSUrlBackgroundRequestExecutor("com.myapp.bg"),
#endif
};
var client = new CosmoClient(options);
The NSURLSession-backed implementation lives in your platform-specific package — Foundation bindings are intentionally not part of this assembly. The full sample below is what you'd drop into a net10.0-ios project (e.g. a MAUI app's iOS head or a CosmoHttpClient.iOS companion package).
Sample NSURLSession bridge
// CosmoHttpClient.iOS/NSUrlBackgroundRequestExecutor.cs
// Target framework: net10.0-ios
using System.Net;
using CosmoHttpClient;
using Foundation;
public sealed class NSUrlBackgroundRequestExecutor : NSObject, ICosmoRequestExecutor, INSUrlSessionDataDelegate
{
private readonly NSUrlSession _session;
public NSUrlBackgroundRequestExecutor(string sessionIdentifier)
{
// Background sessions persist across app suspension. The OS resumes the
// transfer in a separate process and wakes the app to deliver the result.
var cfg = NSUrlSessionConfiguration.CreateBackgroundSessionConfiguration(sessionIdentifier);
cfg.SessionSendsLaunchEvents = true;
cfg.WaitsForConnectivity = true;
cfg.HttpMaximumConnectionsPerHost = 6;
_session = NSUrlSession.FromConfiguration(cfg, (INSUrlSessionDelegate)this, null);
}
public async Task<CosmoResponse> SendAsync(CosmoRequest request, CancellationToken ct)
{
if (request.Uri is null)
throw new InvalidOperationException("CosmoRequest.Uri is required.");
using var nsRequest = new NSMutableUrlRequest(NSUrl.FromString(request.Uri.ToString())!)
{
HttpMethod = request.Method.Name,
};
// Copy user headers. NSURLSession sets Host / Content-Length / connection
// headers itself — skip the ones it manages.
if (request.HeadersOrNull is { } headers)
{
using var hdrDict = new NSMutableDictionary();
foreach (var kv in headers.AsEnumerable())
{
if (string.Equals(kv.Key, "Host", StringComparison.OrdinalIgnoreCase) ||
string.Equals(kv.Key, "Content-Length", StringComparison.OrdinalIgnoreCase) ||
string.Equals(kv.Key, "Connection", StringComparison.OrdinalIgnoreCase))
continue;
hdrDict[kv.Key] = (NSString)kv.Value;
}
nsRequest.Headers = hdrDict;
}
// Body. Background sessions accept either an in-memory body or a file-
// backed upload task. For simplicity, materialize streamed bodies into
// an NSData; replace with a temp-file upload task for very large bodies.
if (!request.Body.IsEmpty)
{
nsRequest.Body = NSData.FromArray(request.Body.ToArray());
}
else if (request.BodyStream is not null)
{
using var ms = new MemoryStream();
await request.BodyStream.CopyToAsync(ms, ct).ConfigureAwait(false);
nsRequest.Body = NSData.FromArray(ms.ToArray());
}
// Run the data task. CreateDataTaskAsync awaits both completion AND any
// background-resume cycle that happened while the app was suspended.
using var ctRegistration = ct.Register(() => { /* see note below */ });
var (data, nsResponse) = await _session.CreateDataTaskAsync(nsRequest).ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
var http = (NSHttpUrlResponse)nsResponse;
var response = new CosmoResponse
{
StatusCode = (int)http.StatusCode,
ReasonPhrase = ((HttpStatusCode)http.StatusCode).ToString(),
};
foreach (var kv in http.AllHeaderFields)
{
string name = kv.Key.ToString();
string value = kv.Value.ToString();
// Cosmo wraps decompression itself; let it see the original
// Content-Encoding header.
response.Headers.Add(name, value);
}
// Body. NSData byte[] is owned by Foundation; copy out so we can dispose
// the NSData without holding the bytes hostage.
response.Body = data is null || data.Length == 0
? Stream.Null
: new MemoryStream(data.ToArray(), writable: false);
return response;
}
// Cancellation: NSURLSessionDataTask exposes Cancel(); wire it through ct
// by holding the task reference and cancelling on the registration callback.
// CreateDataTaskAsync hides the task — for production cancellation support,
// call CreateDataTask(...) directly, register ct => task.Cancel(), and
// bridge the callback delegate to a TaskCompletionSource.
}
Wire it up in MAUI's MauiProgram:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder().UseMauiApp<App>();
builder.Services.AddSingleton<CosmoClient>(_ =>
{
var options = new CosmoHttpOptions
{
#if IOS
RequestExecutorOverride = DeviceInfo.DeviceType == DeviceType.Virtual
? null
: new NSUrlBackgroundRequestExecutor("com.myapp.bg"),
#endif
};
return new CosmoClient(options);
});
return builder.Build();
}
Add the AppDelegate hook so iOS can hand background-session events back to your session:
// AppDelegate.cs (iOS head)
public partial class AppDelegate : MauiUIApplicationDelegate
{
private static readonly Dictionary<string, Action> _backgroundCompletionHandlers = new();
[Export("application:handleEventsForBackgroundURLSession:completionHandler:")]
public override void HandleEventsForBackgroundUrl(
UIApplication application,
string sessionIdentifier,
Action completionHandler)
{
// Stash the completion handler keyed by the session id; your delegate's
// DidFinishEventsForBackgroundSession callback invokes it when iOS has
// delivered every queued event for that session.
_backgroundCompletionHandlers[sessionIdentifier] = completionHandler;
}
internal static void InvokeBackgroundCompletion(string sessionIdentifier)
{
if (_backgroundCompletionHandlers.Remove(sessionIdentifier, out var handler))
handler();
}
}
Inside your INSUrlSessionDelegate.DidFinishEventsForBackgroundSession callback, call AppDelegate.InvokeBackgroundCompletion(session.Configuration.Identifier). iOS will then mark the app fully resumed.
Notes:
- The simulator doesn't support background URL sessions — fall back to the default Cosmo TCP path on virtual devices, as the snippet shows.
- Keep one executor instance per session identifier for the app's lifetime; recreating it abandons in-flight transfers.
- Background transfers are best-effort and slower than foreground ones (iOS schedules them opportunistically). Use this for "must-succeed" requests, not chatty UI calls.
- For large uploads, swap
CreateDataTaskAsyncforCreateUploadTask(NSUrl fileUrl)— Foundation requires a file-backed body for true background uploads.
Limitations and behavior notes
CosmoClientsupportshttp://andhttps://only.CosmoWebSocketsupportsws://andwss://.MaxResponseContentLengthlimits helper-based buffering/materialization paths, not manual streaming fromresponse.Body.- Redirects strip
AuthorizationandProxy-Authorizationon cross-origin hops. CosmoPipelineClientshould only be used against trusted origins with known pipelining support.- HTTP/3 depends on runtime/platform QUIC support.
- The repository supports HTTP/3 TLS session resumption, but not a custom public 0-RTT early-data send API.
- Request compression is explicit; the client does not auto-compress outbound bodies.
- gRPC support is unary only for now — server-streaming, client-streaming, and bidi-streaming RPCs are not yet implemented. Compressed gRPC frames (compression flag
1) are rejected withCosmoGrpcStatus.Internal; configure your server to sendidentity(uncompressed) gRPC payloads. - iOS background URLSession integration is available via
ICosmoRequestExecutor, but the Foundation-binding implementation lives in your iOS-targeted package — this assembly stays platform-neutral and ships only the seam.
Development notes
Common commands:
dotnet build --nologo
dotnet test --nologo --no-build
dotnet run --project samples/CosmoHttpClient.Sample
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks
The current test suite is xUnit-based and exercises HTTP/1.1, HTTP/2, HTTP/3, websockets, caching, DI, auth, resiliency, and telemetry.
| 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.