CosmoApiServer.Core 4.0.14

There is a newer version of this package available.
See the version list below for details.
dotnet add package CosmoApiServer.Core --version 4.0.14
                    
NuGet\Install-Package CosmoApiServer.Core -Version 4.0.14
                    
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="CosmoApiServer.Core" Version="4.0.14" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CosmoApiServer.Core" Version="4.0.14" />
                    
Directory.Packages.props
<PackageReference Include="CosmoApiServer.Core" />
                    
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 CosmoApiServer.Core --version 4.0.14
                    
#r "nuget: CosmoApiServer.Core, 4.0.14"
                    
#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 CosmoApiServer.Core@4.0.14
                    
#: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=CosmoApiServer.Core&version=4.0.14
                    
Install as a Cake Addin
#tool nuget:?package=CosmoApiServer.Core&version=4.0.14
                    
Install as a Cake Tool

CosmoApiServer

A .NET HTTP server built on System.IO.Pipelines and System.Net.Sockets with support for HTTP/1.1, HTTP/2, and HTTP/3 over QUIC. Includes a middleware pipeline, routing, frontend hosting, and real-time primitives.


Contents


Protocols

Protocol Support
HTTP/1.1 Keep-alive, pipelining
HTTP/2 h2c cleartext + ALPN over TLS
HTTP/3 QUIC via System.Net.Quic (libmsquic) on Linux, macOS, and Windows. Request/response trailers, streamed bodies, NDJSON streaming, QPACK, graceful GOAWAY.
TLS SslStream with ALPN (h2 / http/1.1)
var builder = CosmoWebApplicationBuilder.Create()
    .ListenOn(9092)
    .UseHttps("cert.pfx", "password")
    .UseHttp3();

Quick Start

// Minimal API server
var builder = CosmoWebApplicationBuilder.Create().ListenOn(9092);
var app = builder.Build();

app.MapGet("/health", ctx => {
    ctx.Response.WriteJson(new { status = "ok" });
    return ValueTask.CompletedTask;
});

app.Run();

Frontend Integration

All framework integrations share the same three-layer structure:

Layer Purpose Method
Dev server Spawn and manage the frontend process UseViteDevServer / UseAngularDevServer / UseNextDevServer
Dev proxy Forward framework module paths to the dev server UseViteDevProxy / UseReactDevProxy / UseNextDevProxy / UseAngularDevProxy
Production Serve the built output with compression + SPA fallback UseStaticFrontend (or framework-specific wrapper)

Nuxt / Vite Dev Mode

UseViteDevServer spawns the frontend dev process as a managed IHostedService, blocking server startup until it is ready. UseViteDevProxy forwards Vite module-graph paths to the dev server so the browser can use a single origin.

builder.UseViteDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    o.Command          = "npm";
    o.Arguments        = "run dev";
    o.ReadyPattern     = "Local:";
    o.ReadyTimeout     = TimeSpan.FromSeconds(45);
    o.LogPrefix        = "[nuxt]";
});

builder.UseViteDevProxy(o =>
{
    o.DevServerUrl    = "http://127.0.0.1:3000";
    o.ProxiedPrefixes = ["/@vite", "/@fs", "/@id", "/_nuxt", "/__nuxt", "/__vite_ping"];
});

The ViteDevServerService shuts down the child process tree (Kill(entireProcessTree: true)) when the host stops, freeing the dev port cleanly.

Nuxt SSR with server-side API calls (nuxt-auth-utils, useFetch in SSR context)

When Nuxt uses SSR and makes API calls during its startup warmup (e.g. session checks via nuxt-auth-utils), set ReadyPattern = null. This lets the .NET HTTP listener open immediately so the backend is reachable when Nuxt's warmup fires. Without this, the default blocking behaviour delays the listener until after Nuxt is ready — causing ECONNREFUSED during warmup.

builder.UseViteDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    o.Command          = "npm";
    o.Arguments        = $"exec nuxt dev -- --host 127.0.0.1 --port 3000";
    o.ReadyPattern     = null;   // .NET opens immediately; Nuxt starts in background
    o.LogPrefix        = "[nuxt]";
    o.Environment["NUXT_SESSION_PASSWORD"] = "...";
    o.Environment["API_BASE"] = "http://127.0.0.1:9183";
});

// For SSR projects UseReverseProxy forwards page requests; UseNuxtIntegrated is for SPA/static only
builder.UseViteDevProxy(o => o.DevServerUrl = "http://127.0.0.1:3000");
builder.UseReverseProxy(o => o.Routes.Add(new ProxyRoute
{
    PathPrefix       = "/",
    Destination      = "http://127.0.0.1:3000",
    ExcludedPrefixes = ["/api"]
}));

Nuxt Integrated Production

Serves a pre-built Nuxt SPA from .output/public with tiered cache headers, Brotli/GZip compression, and SPA fallback.

builder.UseNuxtIntegrated(
    outputPath: "frontend/.output/public",
    configureFallback: o => o.ExcludedPrefixes = ["/api", "/health"]);

Vue / Vite Production

builder.UseViteFrontend(o =>
{
    o.HtmlTemplatePath = "frontend/index.html";
    o.ManifestPath     = "wwwroot/.vite/manifest.json";
    o.ExcludedPrefixes = ["/api", "/health"];
});

// With server-provided initial state
builder.UseViteFrontend(o =>
{
    o.RenderAsync = ctx => ValueTask.FromResult<ViteRenderResult?>(new ViteRenderResult
    {
        HeadHtml     = "<title>Dashboard</title>",
        InitialState = new { user = "alice", route = ctx.HttpContext.Request.Path }
    });
});

Content Security Policy

Generates a per-request nonce and injects it into inline scripts emitted by ViteFrontendMiddleware.

builder.UseCsp(o =>
{
    o.DefaultSrc = ["'self'"];
    o.ScriptSrc  = ["'self'", "'nonce-{nonce}'"];
    o.StyleSrc   = ["'self'", "'unsafe-inline'"];
    o.ConnectSrc = ["'self'"];
    o.ImgSrc     = ["'self'", "data:"];
});

React + Vite

Dev mode — Vite runs on port 5173 by default:

builder.UseViteDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    o.Command          = "npm";
    o.Arguments        = "run dev";
    o.ReadyPattern     = "Local:";
    o.LogPrefix        = "[react]";
});
builder.UseReactDevProxy();  // proxies /@vite, /@fs, /@id, /@react-refresh
builder.UseViteFrontend(o =>
{
    o.EntryName        = "src/main.tsx";
    o.DevServerUrl     = "http://127.0.0.1:5173";
    o.ExcludedPrefixes = ["/api", "/health"];
});

Productionvite build outputs to dist/ with a manifest:

builder.UseViteFrontend(o =>
{
    o.ManifestPath     = "frontend/dist/.vite/manifest.json";
    o.HtmlTemplatePath = "frontend/index.html";
    o.EntryName        = "src/main.tsx";
    o.ExcludedPrefixes = ["/api", "/health"];
});

Angular

Dev mode — Angular CLI doesn't use Vite, so all traffic is reverse-proxied:

builder.UseAngularDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    // defaults: npx ng serve --host 127.0.0.1, ready pattern "Application bundle generation complete"
});
builder.UseAngularDevProxy();  // reverse-proxies / → http://127.0.0.1:4200

Productionng build outputs to dist/<project>/browser/:

builder.UseAngularFrontend(
    outputPath: "frontend/dist/my-app/browser",
    configureFallback: o => o.ExcludedPrefixes = ["/api", "/health"]);

Next.js

Dev mode — Next.js uses its own HMR paths:

builder.UseNextDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    // defaults: npm run dev, ready pattern "Ready"
});
builder.UseNextDevProxy();  // proxies /__next, /_next, /webpack-hmr
builder.UseViteFrontend(o =>
{
    o.DevServerUrl     = "http://127.0.0.1:3000";
    o.ExcludedPrefixes = ["/api", "/health"];
});

Production (static export) — add output: 'export' to next.config.js, then next build outputs to out/:

builder.UseNextStaticExport(
    outputPath: "frontend/out",
    configureFallback: o => o.ExcludedPrefixes = ["/api", "/health"]);

Production (SSR) — proxy all non-API traffic to next start:

builder.UseReverseProxy(o =>
    o.Routes.Add(new ProxyRoute
    {
        PathPrefix       = "/",
        Destination      = "http://127.0.0.1:3000",
        ExcludedPrefixes = ["/api", "/health"]
    }));

SvelteKit

Dev mode — SvelteKit uses Vite:

builder.UseViteDevServer(o =>
{
    o.WorkingDirectory = "frontend";
    o.Command          = "npm";
    o.Arguments        = "run dev";
    o.ReadyPattern     = "Local:";
    o.LogPrefix        = "[svelte]";
});
builder.UseViteDevProxy(o =>
{
    o.DevServerUrl    = "http://127.0.0.1:5173";
    o.ProxiedPrefixes = ["/@vite", "/@fs", "/@id", "/@svelte-kit"];
});

Production (adapter-static)vite build outputs to build/:

builder.UseSvelteKitStatic(
    outputPath: "frontend/build",
    configureFallback: o => o.ExcludedPrefixes = ["/api", "/health"]);

Production (adapter-node) — proxy to the Node server:

builder.UseReverseProxy(o =>
    o.Routes.Add(new ProxyRoute
    {
        PathPrefix       = "/",
        Destination      = "http://127.0.0.1:3000",
        ExcludedPrefixes = ["/api", "/health"]
    }));

Blazor WebAssembly

Blazor WASM runs entirely in the browser — the .NET runtime and your assemblies are compiled to WebAssembly and downloaded once. CosmoApiServer hosts the published output as static files with two additions over a plain SPA:

  • application/wasm MIME type — browsers reject WASM modules served as application/octet-stream
  • Pre-compressed _framework/ file serving — Blazor's publish pipeline emits .br and .gz variants of every framework file. Streaming these directly (with Content-Encoding: br) is far faster than re-compressing dotnet.native.wasm (30–60 MB) on every request

Setup

  1. Publish your Blazor WASM project to the blazor/wwwroot folder of your CosmoApiServer project:
dotnet publish BlazorApp/BlazorApp.csproj -c Release -o blazor
  1. Register the middleware:
builder.UseBlazorWasm(
    outputPath: "blazor/wwwroot",
    configureFallback: o => o.ExcludedPrefixes = ["/api"]);

The SPA fallback returns index.html for all routes not matched by the API, enabling Blazor's client-side router.

Project structure

Keep the Blazor client project in a subdirectory (e.g. BlazorClient/) of the server project. Because Microsoft.NET.Sdk includes all *.cs files recursively by default, you must exclude the client's source files from the server build:


<ItemGroup>
  <Compile Remove="BlazorClient\**\*.cs" />
</ItemGroup>

Without this, the server compiler picks up Blazor client code (e.g. WebAssemblyHostBuilder) and fails with CS0234.

Co-hosting API + Blazor WASM

var app = builder.Build();

app.MapGet("/api/weather", ctx => { ... });

// Blazor WASM handles everything else

Blazor calls your API endpoints the same way any SPA would — using HttpClient with a base address pointing at the same origin.

Dev proxy paths by framework

Framework Dev server default port Paths to proxy
Nuxt 3000 /@vite /@fs /@id /_nuxt /__nuxt /__vite_ping
React + Vite 5173 /@vite /@fs /@id /@react-refresh
SvelteKit 5173 /@vite /@fs /@id /@svelte-kit
Next.js 3000 /__next /_next /__nextjs_original-stack-frame /webpack-hmr
Angular 4200 Full reverse proxy (no Vite module graph)

Reverse Proxy

builder.UseReverseProxy(o =>
    o.Routes.Add(new ProxyRoute
    {
        PathPrefix       = "/",
        Destination      = "http://127.0.0.1:3000",
        ExcludedPrefixes = ["/api", "/health"]
    }));

Real-time

Server-Sent Events

MapSse sets text/event-stream headers and calls BeginSseAsync automatically.

app.MapSse("/api/live/metrics", async ctx =>
{
    using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
    while (!ctx.RequestAborted.IsCancellationRequested &&
           await timer.WaitForNextTickAsync(ctx.RequestAborted))
    {
        var json = JsonSerializer.Serialize(new { cpu = 42.1, memory = 68.3 });
        await ctx.Response.WriteSseAsync(json, eventName: "metric",
            cancellationToken: ctx.RequestAborted);
    }
});

Heartbeat helper prevents proxies from closing idle connections:

var heartbeat = ctx.Response.SendSseHeartbeatsAsync(TimeSpan.FromSeconds(15), ctx.RequestAborted);
// ... event loop ...
await heartbeat;

SignalR

var builder = CosmoWebApplicationBuilder.Create().AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");

public sealed class ChatHub : Hub
{
    public async Task SendMessage(string user, string message) =>
        await Clients.All.SendAsync("ReceiveMessage", user, message);
}

Supports JSON and MessagePack protocols, groups, server-push via IHubContext<T>, server streaming, cancellation, and reconnect-after-restart. Compatible with standard ASP.NET SignalR clients.

gRPC

var builder = CosmoWebApplicationBuilder.Create().AddGrpc();
var app = builder.Build();
app.MapGrpcService<GreeterService>();

public sealed class GreeterService : GrpcServiceBase, IGrpcServiceDescriptor
{
    public string ServiceName => "Greeter";

    public IReadOnlyList<GrpcMethodDescriptor> Methods =>
    [
        new GrpcMethodDescriptor("Greeter", "SayHello", GrpcMethodType.Unary, typeof(GreeterService),
            (svc, ctx, ct) =>
            {
                var rpc = new GrpcUnaryContext<HelloRequest, HelloReply>(ctx);
                rpc.WriteResponse(new HelloReply { Message = $"Hello {rpc.Request.Name}" });
                return Task.CompletedTask;
            })
    ];
}

HelloRequest/HelloReply are Protobuf messages (IMessage<T>). GrpcUnaryContext decodes the request from the standard 5-byte gRPC framing (rpc.Request) and writes the response (rpc.WriteResponse). Supports unary and server-streaming.


Security

JWT / OAuth / OIDC

builder
    .UseJwtAuthentication(o => o.Secret = "your-signing-secret-at-least-32-bytes")
    .UseOAuthAuthentication(o => o.Authority = "https://issuer.example.com")  // JWKS discovery
    .AddAuthorization();

Antiforgery

builder.AddAntiforgery();
// ...
app.UseAntiforgery();

app.MapGet("/form", ctx =>
{
    var svc = ctx.RequestServices.GetRequiredService<IAntiforgeryService>();
    var tokens = svc.GetAndStoreTokens(ctx);
    return TypedResults.Text($"<input name='__RequestVerificationToken' value='{tokens.RequestToken}' />");
});

Rate Limiting

Fixed-window per-IP limiter with X-Forwarded-For support:

builder.UseRateLimiting(opts =>
{
    opts.Limit      = 200;
    opts.Window     = TimeSpan.FromMinutes(1);
    opts.StatusCode = 429;
    opts.TrustProxy = true;
});

Middleware & Pipeline

Request
  ↓ GlobalExceptionHandlerMiddleware
  ↓ CorsMiddleware
  ↓ CspMiddleware
  ↓ ViteDevProxyMiddleware   (dev)
  ↓ ViteFrontendMiddleware   (dev) / NuxtIntegratedMiddleware (prod)
  ↓ RouterMiddleware

Registration follows a builder pattern:

var builder = CosmoWebApplicationBuilder.Create()
    .ListenOn(9092)
    .UseLogging()
    .UseExceptionHandler()
    .UseCors(o => { o.AllowAnyOrigin(); o.AllowAnyMethod(); o.AllowAnyHeader(); })
    .UseSession(o => o.IdleTimeout = TimeSpan.FromMinutes(20))
    .UseRequestTimeouts(o => o.DefaultTimeout = TimeSpan.FromSeconds(30))
    .UseRateLimiting(opts => { opts.Limit = 100; opts.Window = TimeSpan.FromMinutes(1); })
    .UseForwardedHeaders()
    .AddOutputCache()
    .AddHealthChecks();

Routing

app.MapGet("/items/{id}", ctx => { ... });
app.MapPost("/items", ctx => { ... });

// Typed results
app.MapGet("/items/{id}", ctx =>
    id is null ? TypedResults.NotFound() : TypedResults.Ok(new { id, name = "Widget" }));

Exception Handling

builder
    .AddExceptionHandler<ValidationExceptionHandler>()
    .AddExceptionHandler<DatabaseExceptionHandler>();

public sealed class ValidationExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
    {
        if (ex is not ValidationException vex) return false;
        ctx.Response.StatusCode = 422;
        await ctx.Response.WriteJsonAsync(new { errors = vex.Errors }, ct);
        return true;
    }
}

Scheduling

builder.AddScheduler();
// ...
app.UseScheduler(scheduler =>
{
    scheduler.Schedule(() => Console.WriteLine("tick")).EveryMinute();
    scheduler.ScheduleAsync(async () => await SyncInvoices()).Cron("0 */1 * * *");
    scheduler.Schedule<CleanupJob>().DailyAt(2, 30);
});

Caching

Output Cache

builder.AddOutputCache();
builder.UseOutputCaching();
var app = builder.Build();

var policy = OutputCachePolicy.Build()
    .Expire(TimeSpan.FromMinutes(5))
    .VaryByQuery("page", "sort")
    .Tag("products")
    .ToPolicy();

app.MapGet("/products", async ctx =>
{
    ctx.SetOutputCachePolicy(policy);
    await ctx.Response.WriteJsonAsync(GetProducts());
});

// Tag-based invalidation
var store = ctx.RequestServices.GetRequiredService<IOutputCacheStore>();
await store.EvictByTagAsync("products");

Response Cache (ETag / 304)

builder.UseResponseCaching();
// Handlers set ctx.Response.Headers["ETag"]; returns 304 when If-None-Match matches.

Memory & Distributed Cache

builder
    .AddMemoryCache()
    .AddDistributedMemoryCache();

Diagnostics & Observability

Health Checks

builder.AddHealthChecks()
    .AddCheck("db", () => HealthCheckResult.Healthy("Connected"))
    .AddCheck<MyDbHealthCheck>("database");

builder.UseHealthChecks("/health");   // serves the aggregated report at /health

Problem Details (RFC 7807)

builder.AddProblemDetails();

app.MapGet("/items/{id}", async ctx =>
{
    var svc = ctx.RequestServices.GetRequiredService<IProblemDetailsService>();
    await svc.WriteAsync(new ProblemDetailsContext
    {
        HttpContext    = ctx,
        ProblemDetails = new ProblemDetails { Status = 404, Title = "Not Found" }
    });
});

Distributed Tracing

W3C traceparent propagation, OpenTelemetry-compatible ActivitySource:

builder.UseTracing(o => o.ServiceName = "MyService");

Razor Components

Full .razor support with @page, [Parameter], [CascadingParameter], @inject, @bind, EventCallback, and validation components.

@page "/hello/{Name}"
@inherits Microsoft.AspNetCore.Components.ComponentBase
@inject NavigationManager Nav

<h1>Hello, @Name!</h1>
<p>Path: @Nav.Uri</p>

@code {
    [Parameter] public string Name { get; set; }
}

Forms

<EditForm Model="@person" Action="/contact" Method="post">
    <InputText     Name="name"    Value="@person.Name" />
    <InputNumber   Name="age"     Value="@person.Age" />
    <InputSelect   Name="country" Value="@person.Country">
        <option value="us">United States</option>
    </InputSelect>
    <ValidationMessage For="Name" />
    <ValidationSummary />
    <button type="submit">Submit</button>
</EditForm>

Change Detection

var ctx = new EditContext(model);
model.Name = "Bob";
ctx.NotifyFieldChanged("Name");

ctx.IsModified("Name");      // true
ctx.GetModifiedFields();     // ["Name"]
ctx.FieldCssClass("Name");   // "modified valid"
ctx.MarkAsUnmodified();

Samples

Sample Description
samples/HelloWorldSample Minimal server with a single route
samples/CosmoKitchenSink Covers most backend features in one project
samples/FeatureShowcase Auth, SignalR, gRPC, output cache, and more
samples/WeatherApp REST API with JWT auth, DI, streaming, and SQL
samples/NuxtUiSample Nuxt 4 + Nuxt UI frontend backed by Cosmo APIs
samples/LiveOpsSample Real-time dashboard: SSE, CSP, Vite dev server, Nuxt integrated deployment
samples/CosmoBlazorSample SSR with Razor components
samples/BlazorWasmSample Blazor WebAssembly co-hosted with a CosmoApiServer API

BlazorWasmSample

cd samples/BlazorWasmSample
./run.sh

Publishes the Blazor WASM client to blazor/wwwroot, then starts the CosmoApiServer backend on http://localhost:5050. Includes Notes (CRUD via /api/notes), Counter, and Weather pages.

LiveOpsSample

cd samples/LiveOpsSample
npm run frontend:install
npm run dev

Benchmarks:

npm run benchmark          # API-only latency (no Nuxt)
npm run benchmark:nuxt     # Cosmo integrated vs Nuxt Nitro standalone

NuxtUiSample

cd samples/NuxtUiSample
npm run dev

Benchmarks

Single-client, sequential requests over one reused connection — 100 warmup + 1000 measured per scenario. This measures per-request latency, not concurrent throughput (which would differ). Run with ./run_benchmark.sh; the load driver is tests/ApiServer.Benchmark.

Captured 2026-06-12 on a Debian 12 server (x86-64, bare metal, .NET 10, libmsquic for HTTP/3), CosmoApiServer vs ASP.NET Core (Kestrel) on the identical handlers. P50 latency (ms) and operations/second; lower latency / higher ops is better.

HTTP/1.1 — CosmoApiServer vs ASP.NET Core (Kestrel)

Scenario Cosmo P50 Cosmo ops/s Kestrel P50 Kestrel ops/s
GET /ping 0.10 9,911 0.12 8,039
GET /json 0.10 10,406 0.12 8,599
GET /route/{id} 0.11 9,470 0.11 9,434
POST /echo 0.11 9,425 0.11 9,434
GET /large-json 0.96 1,046 1.01 994
GET /query 0.08 12,453 0.10 9,766
POST /form 0.09 11,601 0.11 9,363
GET /headers 0.09 11,455 0.10 10,020
GET /stream 0.15 6,553 0.09 10,707
GET /file (64 KiB) 0.16 6,072 0.20 4,943

CosmoApiServer leads or ties on most scenarios. GET /file serves hot files (≤256 KiB) from a bounded in-memory cache; larger files stream in 64 KiB chunks. GET /stream flushes per line. (The /file and /stream rows above predate the 2026-06 concurrency work — those paths were since reworked; the table below is the current, authoritative comparison.)

Latency ≠ throughput. This table is single-connection latency. For server capacity, what matters is concurrent throughput, measured separately below — and the two can disagree sharply (an early sendfile change looked like a /file win here while being an 8× concurrency regression). Always judge transport/streaming changes under concurrency.

HTTP/1.1 — concurrent throughput vs ASP.NET Core (Kestrel)

wrk -t4 -c64 -d10s, keep-alive, identical handlers, Debian / 12-core / .NET 10, warm steady-state requests/sec (higher is better) and P99 latency (lower is better). Reproduce with benchmarks/concurrent/run-load.sh.

Scenario Cosmo req/s Kestrel req/s Cosmo/Kestrel Cosmo P99 Kestrel P99
GET /ping 158k 136k 1.17× 8.0ms 10.5ms
GET /json 160k 142k 1.13× 8.2ms 9.3ms
GET /route/{id} 162k 138k 1.18× 8.7ms 14.4ms
GET /large-json 14.2k 13.1k 1.08× 15ms 77ms
GET /stream 100k 130k 0.77× 9.1ms 67ms
GET /file (64 KiB) 32k 39k 0.83× 12.7ms 58ms

Cosmo wins throughput on the small-payload API paths and wins P99 tail latency on every scenario — markedly so on the large/streaming paths (5–7× tighter than Kestrel). Kestrel still leads raw /stream and /file throughput via its socket-native output (Cosmo wraps a NetworkStream), but the gaps closed substantially after the 2026-06 concurrency work: /stream ~2× (drained output pipe) and /file recovered from a 0.59× regression to ~0.83× (small-file cache). See benchmarks/concurrent/README.md.

HTTP/3 (QUIC) — CosmoApiServer

Scenario P50 P99 ops/s
GET /ping 0.21 2.52 4,708
GET /json 0.26 2.32 3,882
GET /route/{id} 0.26 3.15 3,881
POST /echo 0.19 3.38 5,181
GET /large-json 1.21 11.84 824
GET /query 0.17 4.32 5,790
POST /form 0.19 4.37 5,147
GET /headers 0.18 0.25 5,705
GET /stream 0.24 3.60 4,139
GET /file (64 KiB) 0.34 3.26 2,929

HTTP/3 carries higher per-request overhead than plaintext HTTP/1.1 in this single-connection sequential test (QUIC stream setup + crypto). Its real advantages — no head-of-line blocking under loss, and independent per-stream flow control — show under concurrency and lossy networks, which this latency benchmark does not exercise.


Deployment

HTTP/3 in production

Add UseHttp3() with a valid certificate:

var builder = CosmoWebApplicationBuilder.Create()
    .ListenOn(9092)
    .UseHttps("cert.pfx", "password")
    .UseHttp3();

When running Nuxt through UseNuxtIntegrated, all assets — HTML, JS, CSS — are served by Cosmo over the same QUIC connection. SSE streams benefit from QUIC's per-stream flow control, which avoids head-of-line blocking between concurrent requests.

Cloudflare Pages / Workers

Nuxt can be deployed to Cloudflare using the cloudflare_pages preset:

npx nuxi build --preset=cloudflare_pages

This gives HTTP/3 at the Cloudflare edge automatically. However, there are trade-offs relevant to this architecture:

Cloudflare Cosmo + UseHttp3()
HTTP/3 on browser leg Yes (edge) Yes (direct)
SSE streams Fragile — 100s connection timeout on free plan Native
API colocation No — /api/* still needs an origin server Yes
Cloudflare → origin leg HTTP/2 at best N/A

Cloudflare is suitable for frontends that are mostly static or read-heavy without persistent connections. For SSE-heavy or API-coupled deployments, UseHttp3() on Cosmo keeps the entire stack on one connection and one protocol.


Changelog

v4.0.13

  • Connection-specific headers are no longer emitted on HTTP/2 or HTTP/3. Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding, Upgrade — and TE with any value other than trailers — make an HTTP/2 or HTTP/3 message malformed (RFC 9113 §8.2.2, RFC 9114 §4.2). The peer is required to treat it as such, so the response is lost, not merely untidy: the client rejects the field and resets the stream. <br>Both protocols copied the application's response headers verbatim. That made the framework emit an illegal field whenever a handler set one — and handlers are usually right to: Connection: keep-alive is the conventional companion to an SSE response, correct and harmless over HTTP/1.1, and it silently breaks that same handler the moment the connection negotiates HTTP/2. Found by enabling HTTP/2 on a gateway whose SSE endpoints set it: headers arrived, then curl exited 92 with "Invalid HTTP header field was received: name: [connection]" while the server logged nothing wrong. <br>Filtering belongs at the transport, not in application code: a handler cannot know which protocol version its response will be serialised onto, and a framework that forwards an illegal field is the one at fault. HTTP/1.1 is untouched, where these headers are meaningful.

v4.0.12

  • Http2UpstreamForwarder — a reverse-proxy forwarder that reaches upstreams over HTTP/2, multiplexing many client requests onto few TCP connections. PipelineHttpForwarder pools HTTP/1.1 connections for keep-alive reuse, which suits request/response traffic but not streaming: an SSE response never returns its connection to the pool, so the pool degenerates to one upstream socket per client — and on a host-networked proxy dialing a single address:port, one socket per client is one ephemeral port per client, a ceiling in the tens of thousands. Measured: 40 concurrent SSE clients over 1 upstream connection, against 40 for HTTP/1.1. <br>Additive and opt-in by construction — a separate class, so the existing zero-copy HTTP/1.1 path is untouched and no route changes behaviour until it is pointed here. <br>The HTTP/2 client is SocketsHttpHandler: framing, HPACK and flow control are where a bespoke implementation earns subtle bugs, and the runtime's is already correct. What this class owns is the mapping between the two sides and the streaming behaviour SSE depends on — the relay flushes every chunk, because Stream.CopyToAsync flushes when its buffer fills and a heartbeat every few seconds would otherwise reach the client minutes late. <br>Header handling follows the hop-by-hop rules rather than a fixed list: Connection also names hop-local headers (RFC 9110 §7.6.1), and forwarding one would let a client hand hop state to an upstream that trusts it. Set-Cookie is never folded — an Expires date contains a comma, so joining two cookies yields one corrupt cookie and a silently lost session. <br>Connect-phase failures — refusal, TLS/ALPN rejection, and connect timeout — all surface as UpstreamConnectException, so failover-aware callers get the retry the 4.0.7 taxonomy promises. Caller cancellation deliberately does not: a client that gave up is not a broken upstream, and marking one unhealthy would fail traffic away from a working server. <br>That classification is proven, not inferred. A dropped connection reports the same ConnectionError whether it failed to connect or died after a POST was sent and acted on, so inferring "nothing moved" from the error kind would let a caller replay an already-processed request and duplicate its side effects. Connect failures are therefore raised by ConnectCallback, which runs only while establishing a connection — a post-send failure has no way to be mistaken for one.

v4.0.11

  • HTTP/2 can stream a response body. Http3Connection has installed a StreamingResponseWriter since it shipped; Http2Connection never did, so a streaming handler fell through to HttpResponse's buffered fallback — the whole body collected in a MemoryStream and written only once the body writer returned. For a response that never returns (SSE) nothing was ever sent, and the client waited until the connection died. Latent rather than harmless: any server that enabled HTTP/2 would have had every streaming endpoint hang, with no error to say why. <br>HEADERS now go out immediately and each write becomes flow-controlled DATA frames. Credit is taken per write rather than reserved for the whole body (its length is unknown), and the connection write lock is held only for that write's frames — a long-lived stream holding either would head-of-line block every other stream on the connection, which is the one thing multiplexing exists to prevent. A peer-reset stream surfaces as ClientDisconnectedException. <br>Two neighbouring gaps came out of the same work. SendResponseAsync was unguarded, so a completed streaming response was followed by the buffered response — a second HEADERS block on a closed stream, which takes the connection down rather than just that stream; Http11Connection has guarded this since streaming existed, HTTP/2 simply had no streaming path to guard until now. And a handler that fails partway now ends the stream with RST_STREAM(INTERNAL_ERROR) instead of leaving it open forever: once HEADERS are sent there is no status left to change, and ending normally would tell the client it had received a complete response. <br>This is what lets many SSE clients share one upstream connection: 40 concurrent long-lived streams over a single TCP socket, verified end to end against a real HTTP/2 client.

v4.0.10

  • ListenBacklog is configurable instead of hardcoded at 4096. The kernel silently caps the backlog at net.core.somaxconn, so asking for more than a host allows is harmless — but asking for less is not, and the hardcoded value quietly discarded the headroom on any host tuned above it (a gateway host raised to somaxconn = 65535 still listened 4096 deep). Depth here is what absorbs a burst of SYNs while the accept loop catches up. It is not free: a long queue makes a client wait rather than fail fast, so it helps against bursts, not against genuine saturation. Default is unchanged at 4096, and a non-positive value falls back to it rather than failing to listen.

v4.0.9

Connection capacity that follows the box, and disconnects that read as disconnects.

  • MaxConcurrentConnections now defaults to the descriptor limit less a reserve instead of a fixed 10,000. The cap exists to protect file descriptors, so a fixed number was wrong in both directions at once: too high for a container left at the 1024 default, and ~50x too low for one provisioned at 524,288, where it silently became the binding limit on capacity long before any real resource was short. Derivation lives in ConnectionLimits: the reserve scales down on small limits (1024 descriptors → 768 connections) so the cap always stays under the limit it protects, and the result is clamped to a 1,000,000 default ceiling. Off Linux, or where /proc/self/limits cannot be read, it falls back to the previous 10,000. Set the option explicitly to override — including above the ceiling — or to 0 for no limit.
  • ClientDisconnectedException distinguishes "the peer went away" from a server fault. A client vanishing mid-response is routine on any long-lived response — every mobile SSE client that changes network produces one — but it surfaced through the global handler as a full [ERROR] and stack trace. It now logs a single [INFO] line and skips the 500 response there is nobody left to read. The type derives from IOException, so existing catch (IOException) handlers are unaffected. Classification also covers the raw socket errors the OS reports for the same event (EPIPE/Shutdown, ConnectionReset, ConnectionAborted, OperationAborted), which is the shape the transport actually throws.

v4.0.8

  • A streaming handler now learns when its client disconnects. PipeWriter.FlushAsync reports a finished reader through FlushResult.IsCompleted rather than by throwing, and the drained streaming path discarded it — so an endless response (SSE) went on "successfully" writing into a connection whose peer had left, holding its socket, its buffers, and its connection permit for the life of the process. The permit leak was the operational half: enough abandoned streams and the server stops accepting new connections while looking healthy. The flush now surfaces the completed reader, and the drain loop completes its reader with the failure so the exception reaches the handler instead of being swallowed at the transport boundary.

v4.0.7

Reverse-proxy forwarder — failure taxonomy that load balancers can build on.

  • UpstreamConnectException now wraps TCP-connect and TLS-handshake failures. The type encodes a guarantee: no application byte moved in either direction, so a caller may retry a different upstream with no idempotency concerns — POST included. A caller's own cancellation is deliberately not wrapped: a client abort is not an upstream failure and must not trip failover or health marking. (CosmoProxy's passive failover is built on exactly this.)
  • Transparent stale-pool retry. A request written into a pooled keep-alive connection the upstream had already closed (restart, idle reap, LB recycle — TCP keepalive hasn't faulted it yet) is now retried once on a fresh connection instead of failing. Guarded three ways: the body must be absent or fully buffered (a streamed body was partially consumed and cannot be replayed), no response may have been parsed (nothing relayed to the client, no Set-Cookie duplication), and past a completed flush only idempotent methods are replayed — an incomplete request cannot have been acted on, a fully-sent POST can no longer be proven side-effect-free. A failed fresh reconnect surfaces as UpstreamConnectException, so the two mechanisms compose into one escalation path.

v3.5.1

  • Razor slices on .NET 10 — the .NET 10 Razor SDK's generated .cshtml view code references types the framework's compile-time stubs didn't cover, which broke every RazorSlice project. Added stubs (IModelExpressionProvider, RazorCompiledItem/RazorCompiledItemMetadata/ ProvideApplicationPartFactory attributes) so .cshtml slices compile without the ASP.NET MVC framework again.
  • Samples — all sample projects build (fixed a stale project-reference path, an out-of-date action-filter override signature, and missing Blazor WASM usings).

v3.5.0

Security, protocol-correctness, and performance release.

  • Security — closed an endpoint-filter/RequireAuthorization bypass (filters were silently dropped), fail-open [Authorize] (now fails closed), output/response cache session poisoning (no longer caches authenticated / Set-Cookie / no-store responses; keys on Host), stored XSS in HtmlEditorComponent (encoded by default), JWT access_token query param accepted on every request (now WebSocket-upgrade only), component-POST mass-assignment ([BindProperty] allow-list + antiforgery), and request-smuggling vectors (strict Content-Length / Transfer- Encoding, chunked-trailer handling, percent-decoded routing, proxy hop-by-hop + surplus-byte handling).
  • HTTP/2 — fixed a truncated HPACK Huffman table that tore down the connection on any Huffman-coded header (broke H2 for real clients); response bodies now framed across DATA frames with HEADERS+CONTINUATION; pseudo-header validation; flow-control accounting with WINDOW_UPDATE validation, PING-flood and rapid-reset (CVE-2023-44487) mitigation.
  • HTTP/3 — pseudo-header validation, Content-Length vs DATA reconciliation, frame-length caps, unknown-stream tolerance; the dynamic-QPACK encoder is gated off for interop safety.
  • DoS hardening — request body / header-section / connection caps, streamed multipart, bounded QPACK decoder state and template/antiforgery caches. New ServerOptions: MaxConcurrentConnections, MaxRequestHeaderSize, MaxRequestHeaderCount, Http3MaxFrameSize.
  • Performance — zero-copy sendfile for whole-file responses on plaintext HTTP/1.1; buffered JSON serialized directly into the response buffer; idle-vs-active connection timeouts so streaming/SSE/WebSocket connections aren't dropped while active; scheduler/cron and OpenAPI generator fixes.
  • DependencyMessagePack bumped to 2.5.301 (clears advisory GHSA-hv8m-jj95-wg3x).
  • 637 tests.

v3.2.3

  • BlazorWasmSample — new sample: Notes CRUD + Counter + Weather, Blazor WASM co-hosted with CosmoApiServer
  • UseBlazorWasm project structure: document <Compile Remove="BlazorClient\**\*.cs" /> requirement when client lives in a subdirectory of the server project

v3.2.2

  • UseBlazorWasm — hosts published Blazor WebAssembly apps with pre-compressed _framework/ file serving and application/wasm MIME type
  • StaticFileMiddleware — added .wasm → application/wasm to the MIME table

v3.2.0

  • Frontend integration for React + Vite, Angular, Next.js, and SvelteKit
  • UseStaticFrontend(outputPath) — generic base for all static SPA deployments
  • UseAngularFrontend, UseNextStaticExport, UseSvelteKitStatic — framework-specific wrappers
  • UseReactDevProxy, UseNextDevProxy, UseAngularDevProxy — pre-configured dev proxies per framework
  • UseAngularDevServer, UseNextDevServer — pre-configured dev server process management
  • LiveOpsSample: benchmark scripts (run-benchmark.sh, run-nuxt-benchmark.sh)
  • Documentation rewrite — structured, framework-agnostic

v3.1.0

  • Server-Sent Events, Content Security Policy, Vite Dev Proxy, Vite Dev Server, Nuxt Integrated, Reverse Proxy
  • samples/LiveOpsSample demonstrating all six features
  • Bug fixes: CORS origin spoofing guard, CSRF path bypass, Retry-After off-by-one, ForwardedHeaders trusted-proxy gate, AntiforgeryMiddleware Content-Type guard, SPA fallback cache-control
  • 373 tests

v3.0.3

  • Vue frontend hosting via ViteFrontendMiddleware
  • samples/NuxtUiSample

v3.0.2

  • HtmlEditorComponent for Razor slices

v3.0.1

  • Razor: InputDate, InputRadioGroup, InputRadio, InputFile, RenderTreeBuilder stubs

v3.0.0

  • HTTP/3 production-ready
  • Rate Limiting
  • H3Interop validation tool (32/32 scenarios)
  • Windows benchmark scripts
  • 313 tests

v2.1.0 — v2.1.4

  • SignalR (JSON + MessagePack), gRPC, Sessions, Request Timeouts, Response Caching, Forwarded Headers, Request Decompression, Distributed Tracing, Endpoint Filters, IHttpContextAccessor
  • Output Caching, Antiforgery, TypedResults, IExceptionHandler, IHostedService, WebSockets

v2.0.5 — v2.0.7

  • Health Checks, Problem Details, Policy-Based Authorization, OAuth/OIDC, Memory Cache, Distributed Cache, IHttpClientFactory
  • Stream flush coalescing, HTTP/3 QPACK, trailers, GOAWAY

Credits

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 (1)

Showing the top 1 NuGet packages that depend on CosmoApiServer.Core:

Package Downloads
CosmoS3

Amazon S3-compatible object storage server built on CosmoApiServer.Core.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.21 58 9/3/2026
4.0.20 57 9/3/2026
4.0.19 66 9/3/2026
4.0.18 66 9/2/2026
4.0.17 65 9/2/2026
4.0.16 66 9/2/2026
4.0.15 69 9/2/2026
4.0.14 75 9/2/2026
4.0.13 71 9/2/2026
4.0.12 70 9/2/2026
4.0.11 68 9/2/2026
4.0.10 73 9/1/2026
4.0.9 67 9/1/2026
4.0.8 81 9/1/2026
4.0.7 121 8/3/2026
4.0.6 113 8/2/2026
4.0.5 110 8/2/2026
4.0.4 111 7/30/2026
4.0.3 198 7/29/2026
4.0.2 118 7/28/2026
Loading failed