CosmoHttpClient 1.0.0
See the version list below for details.
dotnet add package CosmoHttpClient --version 1.0.0
NuGet\Install-Package CosmoHttpClient -Version 1.0.0
<PackageReference Include="CosmoHttpClient" Version="1.0.0" />
<PackageVersion Include="CosmoHttpClient" Version="1.0.0" />
<PackageReference Include="CosmoHttpClient" />
paket add CosmoHttpClient --version 1.0.0
#r "nuget: CosmoHttpClient, 1.0.0"
#:package CosmoHttpClient@1.0.0
#addin nuget:?package=CosmoHttpClient&version=1.0.0
#tool nuget:?package=CosmoHttpClient&version=1.0.0
CosmoHttpClient
CosmoHttpClient is a .NET 10 HTTP client library built around System.IO.Pipelines and Cosmo.Transport. The repository contains:
- a pooled HTTP/1.1 client optimized for low allocation streaming
- protocol-specific HTTP/2 and HTTP/3 clients
- a universal client that learns per-origin protocol preference
- WebSocket support
- an HTTP/1.1 pipelining client for trusted origins
- tests, a runnable sample, and BenchmarkDotNet benchmarks (HTTP/1.1 + HTTP/2 h2c)
Recent additions
- Unary gRPC client wire-compatible with CosmoApiServer (
CosmoHttp2Client.SendGrpcAsync<TReq, TRes>). - NDJSON streaming reader —
CosmoResponse.ReadBodyAsNdjsonAsync<T>()yieldsIAsyncEnumerable<T>for newline-delimited JSON endpoints. - CosmoApiServer tuning profile —
CosmoHttpTuningProfiles.ForCosmoApiServer()aligns the negotiation surface with what CosmoApiServer actually accepts (gzip-only decompression, HTTP/2 keepalive, longer connection lifetime). - Request-level extension point —
ICosmoRequestExecutorfor plugging iOS background URLSession (or any other request-shaped transport) without forking the library. - Generic Content-Length fast path in
ReadBodyAsBytesAsynccuts HTTP/2 download allocations by ~58% (was 180 KB / 64 KiB body, now ~75 KB). - HTTP/2 correctness + performance fixes — atomic flow-control windows (no Task.Delay polling), per-stream window honored, silent-truncation fix on body read with decompression, fire-and-forget tasks no longer leak unobserved exceptions.
What it supports
Core transports
| Surface | Purpose |
|---|---|
CosmoClient |
Main HTTP/1.1 client with pooling, retries, redirects, cookies, decompression, caching, and telemetry |
CosmoHttp2Client |
HTTP/2 client with multiplexing and server push support |
CosmoHttp3Client |
HTTP/3 client over QUIC with TLS resumption support |
CosmoUniversalClient |
Chooses HTTP/1.1, HTTP/2, or HTTP/3 per origin and learns from Alt-Svc |
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 |
tests/CosmoHttpClient.Tests |
xUnit test suite (277 tests) |
samples/CosmoHttpClient.Sample |
Runnable sample app |
benchmarks/CosmoHttpClient.Benchmarks |
BenchmarkDotNet benchmarks (HTTP/1.1 + HTTP/2) |
benchmarks/CosmoHttpClient.Microbench |
Component-level micro-benchmarks |
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
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
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.
CosmoUniversalClient
CosmoUniversalClient wraps HTTP/1.1, HTTP/2, and HTTP/3:
http://origins start on HTTP/1.1https://origins start on HTTP/2Alt-Svccan promote an origin to HTTP/3- failed HTTP/2 or HTTP/3 attempts are demoted and remembered
await using var client = new CosmoUniversalClient();
await using var response = await client.GetAsync("https://www.cloudflare.com/");
You can pin an origin explicitly:
client.PinProtocol(new Uri("https://example.com/"), CosmoHttpVersion.Http2);
CosmoHttp2Client
Use CosmoHttp2Client when you want explicit HTTP/2 behavior:
- multiplexed requests over one connection
- HPACK
- optional server push handling into the response cache
- h2c cleartext for
http:// - unary gRPC via
SendGrpcAsync<TReq, TRes>(...)extension (see gRPC)
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
CosmoHttp2Client ships a unary-gRPC extension 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). The package stays payload-format-agnostic — you supply the serialize and deserialize delegates, so you can plug Google.Protobuf, protobuf-net, MessagePack, or anything else without forcing a heavy dependency.
using CosmoHttpClient.Grpc;
await using var client = new CosmoHttp2Client(
new CosmoHttpOptions { /* ... */ },
useCleartext: true); // h2c for trusted networks; omit for h2-over-TLS
var response = await client.SendGrpcAsync(
origin: new Uri("https://api.example.com"),
serviceMethodPath: "/cosmo.Greeter/SayHello",
request: new HelloRequest { Name = "world" },
serialize: req => req.ToByteArray(), // your serializer
deserialize: bytes => HelloReply.Parser.ParseFrom(bytes.Span),
metadata: new Dictionary<string, string> { ["x-tenant"] = "alice" });
if (response.IsOk)
Console.WriteLine(response.Value!.Message);
else
Console.WriteLine($"gRPC failed: {response.Status} — {response.Message}");
// Or, throw on non-OK:
HelloReply reply = response.EnsureSuccess();
Unary RPC only for now. Server-streaming / client-streaming / bidi-streaming aren't wired yet. Compressed frames are rejected (the spec compression flag must be 0).
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/2 and 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 against Microsoft HttpClient over a loopback Kestrel server, for both HTTP/1.1 and HTTP/2 (h2c). Each suite covers four scenarios: GET plaintext 1 KiB, GET JSON, POST echo 4 KiB, download 64 KiB.
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.
HTTP/1.1
| Scenario | CosmoHttpClient | 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 |
HTTP/2 (h2c)
| Scenario | CosmoHttp2Client | HttpClient |
|---|---|---|
| GET plaintext 1 KiB | 53.41 us, 4.93 KB | 51.95 us, 4.00 KB |
| GET JSON | 49.75 us, 4.67 KB | 58.36 us, 3.34 KB |
| POST echo 4 KiB | 75.85 us, 4.27 KB | 67.30 us, 5.23 KB |
| Download 64 KiB | 148.83 us, 74.75 KB | 130.47 us, 69.70 KB |
Benchmark reports (markdown + CSV + HTML) are written under BenchmarkDotNet.Artifacts/results/ after each run.
To re-run a single suite:
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*Http11ClientBenchmarks*'
dotnet run -c Release --project benchmarks/CosmoHttpClient.Benchmarks -- --filter '*Http2ClientBenchmarks*'
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.