CosmoHttpClient 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package CosmoHttpClient --version 1.0.0
                    
NuGet\Install-Package CosmoHttpClient -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CosmoHttpClient" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CosmoHttpClient" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="CosmoHttpClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CosmoHttpClient --version 1.0.0
                    
#r "nuget: CosmoHttpClient, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CosmoHttpClient@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CosmoHttpClient&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=CosmoHttpClient&version=1.0.0
                    
Install as a Cake Tool

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 readerCosmoResponse.ReadBodyAsNdjsonAsync<T>() yields IAsyncEnumerable<T> for newline-delimited JSON endpoints.
  • CosmoApiServer tuning profileCosmoHttpTuningProfiles.ForCosmoApiServer() aligns the negotiation surface with what CosmoApiServer actually accepts (gzip-only decompression, HTTP/2 keepalive, longer connection lifetime).
  • Request-level extension pointICosmoRequestExecutor for plugging iOS background URLSession (or any other request-shaped transport) without forking the library.
  • Generic Content-Length fast path in ReadBodyAsBytesAsync cuts 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 ActivitySource and Meter
  • 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>() — streaming IAsyncEnumerable<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.Default
  • GetAsync
  • SendAsync
  • GetFromJsonAsync<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.1
  • https:// origins start on HTTP/2
  • Alt-Svc can 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:// and wss://
  • 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:

  • Headers
  • CookieContainer
  • CredentialProvider
  • Body
  • BodyStream
  • BodyLength

Helper methods include:

  • SetUri
  • SetTextBody
  • SetBytesBody
  • SetJsonBody
  • SetMultipartBody
  • SetCompressedBody
  • SetBearerToken
  • SetBasicAuth

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.duration
  • http.client.active_requests
  • http.client.request.body.size
  • http.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 CreateDataTaskAsync for CreateUploadTask(NSUrl fileUrl) — Foundation requires a file-backed body for true background uploads.

Limitations and behavior notes

  • CosmoClient supports http:// and https:// only.
  • CosmoWebSocket supports ws:// and wss://.
  • MaxResponseContentLength limits helper-based buffering/materialization paths, not manual streaming from response.Body.
  • Redirects strip Authorization and Proxy-Authorization on cross-origin hops.
  • CosmoPipelineClient should 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 with CosmoGrpcStatus.Internal; configure your server to send identity (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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
5.1.1 91 5/29/2026
5.1.0 1,093 5/24/2026
5.0.5 243 5/21/2026
5.0.4 89 5/21/2026
5.0.3 102 5/18/2026
5.0.2 109 5/18/2026
5.0.1 90 5/18/2026
5.0.0 105 5/10/2026
4.0.0 105 5/10/2026
3.0.0 99 5/9/2026
2.0.0 97 5/9/2026
1.0.0 99 5/8/2026