diff --git a/.gitignore b/.gitignore index 4805f7003..f6e488803 100644 --- a/.gitignore +++ b/.gitignore @@ -344,6 +344,7 @@ docs/superpowers/* COMMIT.md .tools/ coverage-results/ +results/ .maggus/runs .maggus/MEMORY.md .maggus/RELEASE_NOTES.md diff --git a/CLAUDE.md b/CLAUDE.md index ec1d105de..2f3f5b7f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,10 +35,9 @@ cd ../docs && npm install && npm run docs:dev ``` Client Surface (TurboHTTP/Client/) - ITurboHttpClient, factory, builder, DI, options -Server Surface (TurboHTTP/Server/) - TurboServerOptions, TurboHttpContext, Hosting, Middleware -Context (TurboHTTP/Context/) - TurboHttpRequest, TurboHttpResponse, Adapters, Features -Routing (TurboHTTP/Routing/) - RouteTable, dispatchers, Binding/ -Streams Layer (TurboHTTP/Streams/) - Engines, Stages/{Client,Server,Features,Routing}, Lifecycle, Pooling +Server Surface (TurboHTTP/Server/) - TurboServer (IServer), TurboServerOptions, Hosting, FeatureCollectionFactory +Context (TurboHTTP/Context/) - Features/ (IHttp*Feature implementations), Adapters/ +Streams Layer (TurboHTTP/Streams/) - Engines, Stages/{Client,Server,Features}, Lifecycle, Pooling Protocol Layer (TurboHTTP/Protocol/) - Http10/, Http11/, Http2/, Http3/, Semantics/ Features (TurboHTTP/Features/) - Cookies/, Caching/, AltSvc/ Diagnostics (TurboHTTP/Diagnostics/) - Metrics, tracing, logging diff --git a/docs/.vitepress/components/HomePage.vue b/docs/.vitepress/components/HomePage.vue index 0ccf94064..11ab3539c 100644 --- a/docs/.vitepress/components/HomePage.vue +++ b/docs/.vitepress/components/HomePage.vue @@ -13,8 +13,8 @@ const features = [ description: 'Idempotency-aware retries + in-memory LRU cache with ETag support. Built in, not bolted on.', }, { - title: 'Middleware & Routing', - description: 'ASP.NET Core-style pipeline with Use/Run/Map. Entity gateway routes requests to Akka.NET actors.', + title: 'IServer Drop-In', + description: 'Replaces Kestrel as IServer — use standard ASP.NET Core middleware, routing, and DI unchanged.', }, { title: 'Connection Pooling', @@ -49,13 +49,13 @@ const clientCode = `builder.Services.AddTurboHttpClient("api", options => var client = factory.CreateClient("api"); var response = await client.SendAsync(request, ct);` -const serverCode = `builder.Services.AddTurboKestrel(options => +const serverCode = `builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); }); -app.MapTurboGet("/health", () => new { status = "ok" }); -app.MapTurboGet("/users/{id}", (int id) => +app.MapGet("/health", () => new { status = "ok" }); +app.MapGet("/users/{id}", (int id) => new { id, name = "User " + id }); await app.RunAsync();` diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 5eaf5d718..64ae7a0d3 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -76,13 +76,9 @@ export default defineConfig({ { text: 'Overview', link: '/server/' }, { text: 'Installation & Setup', link: '/server/installation' }, { text: 'Configuration', link: '/server/configuration' }, + { text: 'Using with ASP.NET Core', link: '/server/aspnet-core' }, { text: 'Hosting & Lifecycle', link: '/server/hosting' }, - { text: 'Middleware Pipeline', link: '/server/middleware' }, - { text: 'Routing', link: '/server/routing' }, - { text: 'Parameter Binding', link: '/server/binding' }, - { text: 'Validation', link: '/server/validation' }, - { text: 'Entity Gateway', link: '/server/entity-gateway' }, - { text: 'Real-World Scenarios', link: '/server/scenarios' }, + { text: 'Performance Tuning', link: '/server/performance' }, { text: 'Troubleshooting', link: '/server/troubleshooting' }, ], }, @@ -96,7 +92,6 @@ export default defineConfig({ { text: 'Client Options', link: '/api/client-options' }, { text: 'Feature Options', link: '/api/feature-options' }, { text: 'Server API', link: '/api/server' }, - { text: 'Entity Gateway API', link: '/api/entity-gateway' }, ], }, ], diff --git a/docs/api/entity-gateway.md b/docs/api/entity-gateway.md deleted file mode 100644 index 3ef5c3fce..000000000 --- a/docs/api/entity-gateway.md +++ /dev/null @@ -1,322 +0,0 @@ -# Entity Gateway API - -The Entity Gateway provides actor-based request routing. Incoming HTTP requests are routed to actors based on an entity key extracted from the URL, enabling stateful per-entity request handling. - -## Registration - -Register entity routes via `MapTurboEntity` on `WebApplication`: - -```csharp -public static class TurboRoutingExtensions -{ - TurboRouteHandlerBuilder MapTurboEntity( - this WebApplication app, - string pattern, - Action configure); - - TurboRouteHandlerBuilder MapTurboEntity( - this WebApplication app, - string pattern, - Action configure); -} -``` - -### Untyped Key - -When no type parameter is provided, entity keys are boxed `object`: - -```csharp -app.MapTurboEntity("/users/{id}", entity => -{ - entity.UseActorRef(); - entity.OnGet((string id) => new GetUser(id)); -}); -``` - -### Typed Key - -Specify a typed key for type safety: - -```csharp -app.MapTurboEntity("/orders/{orderId}", entity => -{ - entity.UseResolver(); - entity.OnGet((int orderId) => new GetOrder(orderId)); - entity.OnPost((int orderId, CreateOrderRequest req) => new CreateOrder(orderId, req.Items)); -}); -``` - -Inside route groups: - -```csharp -var api = app.MapTurboGroup("/api"); - -api.MapEntity("/users/{id}", builder => { /* ... */ }); -api.MapEntity("/posts/{slug}", builder => { /* ... */ }); -``` - ---- - -## TurboEntityBuilder - -```csharp -public sealed class TurboEntityBuilder -{ - public TurboEntityMethodBuilder OnGet(Delegate messageFactory); - public TurboEntityMethodBuilder OnPost(Delegate messageFactory); - public TurboEntityMethodBuilder OnPut(Delegate messageFactory); - public TurboEntityMethodBuilder OnDelete(Delegate messageFactory); - public TurboEntityMethodBuilder OnPatch(Delegate messageFactory); - - public TurboEntityBuilder MapResponse( - Func mapper); - - public TurboEntityBuilder WithTimeout(TimeSpan timeout); - - public TurboEntityBuilder UseResolver(IEntityActorResolver resolver); - public TurboEntityBuilder UseResolver() where TResolver : IEntityActorResolver, new(); - - public TurboEntityBuilder UseActorRef(); - public TurboEntityBuilder UseActorRef(Func factory); - public TurboEntityBuilder UseActorRef(Func actorRefFactory); -} -``` - -### HTTP Method Handlers - -Specify what message to send for each HTTP method: - -```csharp -entity.OnGet((int id) => new GetUser(id)); -entity.OnPost((int id, CreateUserRequest req) => new CreateUser(id, req.Name)); -entity.OnPut((int id, UpdateUserRequest req) => new UpdateUser(id, req.Name)); -entity.OnDelete((int id) => new DeleteUser(id)); -entity.OnPatch((int id, PatchUserRequest req) => new PatchUser(id, req)); -``` - -Each handler receives typed parameters from the route and request body, and returns a message to send to the actor. - -### Response Mapping - -By default, responses from actors are serialized as JSON. Customize mapping with `MapResponse`: - -```csharp -entity.MapResponse(async (context, user) => -{ - context.Response.StatusCode = 200; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsJsonAsync(user); -}); - -entity.MapResponse(async (context, error) => -{ - context.Response.StatusCode = error.StatusCode; - await context.Response.WriteAsJsonAsync(new { error = error.Message }); -}); -``` - -### Timeout Configuration - -Set per-route timeout for actor responses: - -```csharp -// Default: 30 seconds -entity.WithTimeout(TimeSpan.FromSeconds(60)); -``` - -If the actor doesn't respond within the timeout, the response is `504 Gateway Timeout`. - ---- - -## TurboEntityMethodBuilder - -```csharp -public sealed class TurboEntityMethodBuilder -{ - public TurboEntityMethodBuilder AcceptedResponse(); - public TurboEntityMethodBuilder WithTimeout(TimeSpan timeout); -} -``` - -### AcceptedResponse - -Respond with `202 Accepted` instead of waiting for the actor's full response: - -```csharp -entity.OnPost((int id, CreateUserRequest req) => new CreateUser(id, req.Name)) - .AcceptedResponse(); -``` - -Useful for long-running operations where you want to return immediately. - -### Per-Method Timeout - -Override the route-level timeout for a specific method: - -```csharp -entity.OnGet((int id) => new GetUser(id)) - .WithTimeout(TimeSpan.FromSeconds(5)); - -entity.OnPost((int id, CreateUserRequest req) => new CreateUser(id, req.Name)) - .WithTimeout(TimeSpan.FromSeconds(30)); -``` - ---- - -## Resolver Strategies - -Entity routes need a strategy to locate or create the actor that will handle the request. TurboHTTP supports three approaches: - -### 1. Custom Resolver - -Implement `IEntityActorResolver` for full control: - -```csharp -public interface IEntityActorResolver -{ - IActorRef ResolveActor(TKey key); -} - -public class OrderActorResolver : IEntityActorResolver -{ - private readonly IActorRef _orderManager; - - public OrderActorResolver(IActorRef orderManager) - { - _orderManager = orderManager; - } - - public IActorRef ResolveActor(TKey key) - { - // Route to shard based on order ID - return _orderManager; - } -} - -// Register it -entity.UseResolver(new OrderActorResolver(orderManagerRef)); -// Or as a type -entity.UseResolver(); -``` - -### 2. ActorRef Factory - -Direct reference to a specific actor: - -```csharp -// From dependency injection -entity.UseActorRef((serviceProvider) => -{ - return serviceProvider.GetRequiredService("userActorRef"); -}); - -// From actor registry -entity.UseActorRef((registry) => -{ - return registry.Resolve(); -}); -``` - -### 3. Generic ActorRef Lookup - -Use the type system to locate actors by type: - -```csharp -entity.UseActorRef(); -``` - -This looks up `UserActor` in the actor registry. - ---- - -## Complete Example - -```csharp -// Actor definition -public class UserActor : ReceiveActor -{ - public sealed class GetUser { public int Id { get; set; } } - public sealed class CreateUser { public string Name { get; set; } } - public sealed class User { public int Id { get; set; } public string Name { get; set; } } - - public UserActor() - { - Receive(msg => - { - // Simulate database lookup - var user = new User { Id = msg.Id, Name = "John Doe" }; - Sender.Tell(user); - }); - - Receive(msg => - { - var user = new User { Id = 123, Name = msg.Name }; - Sender.Tell(user); - }); - } -} - -// Route registration -app.MapTurboEntity("/users/{id}", entity => -{ - entity.UseActorRef(); - - entity.OnGet((int id) => new UserActor.GetUser { Id = id }); - entity.OnPost((int id, CreateUserRequest req) => new UserActor.CreateUser { Name = req.Name }); - - entity.MapResponse(async (context, user) => - { - context.Response.ContentType = "application/json"; - await context.Response.WriteAsJsonAsync(user); - }); - - entity.WithTimeout(TimeSpan.FromSeconds(30)); -}); -``` - ---- - -## Request Flow - -1. HTTP request arrives for `/users/123` -2. Route pattern matches; entity key `123` is extracted -3. Message factory is called: `OnGet(ctx) => new GetUser { Id = 123 }` -4. Resolver locates or creates the actor -5. Message is sent to the actor (ask pattern with timeout) -6. Actor responds with a result (e.g., `User` object) -7. Response mapper serializes the result to HTTP response -8. Response is sent to the client - -If the actor doesn't respond within the timeout, the response is `504 Gateway Timeout`. If `AcceptedResponse()` is used, step 8 returns `202 Accepted` immediately after sending the message. - ---- - -## Error Handling - -If the actor throws an exception or the message doesn't match a handler, the gateway responds with `500 Internal Server Error`. Use status code routing or custom response mappers to handle errors from the actor: - -```csharp -entity.MapResponse>(async (context, result) => -{ - if (result.IsSuccess) - { - context.Response.StatusCode = 200; - await context.Response.WriteAsJsonAsync(result.Value); - } - else - { - context.Response.StatusCode = 400; - await context.Response.WriteAsJsonAsync(new { error = result.Error }); - } -}); -``` - -Or use a wrapper response type: - -```csharp -entity.MapResponse(async (context, response) => -{ - context.Response.StatusCode = response.StatusCode; - await context.Response.WriteAsJsonAsync(response); -}); -``` diff --git a/docs/api/index.md b/docs/api/index.md index 9a43ecc8c..756a134be 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -17,13 +17,12 @@ TurboHTTP's public API is organized into client, server, and feature configurati | Type | Description | Reference | |------|-------------|-----------| -| `AddTurboKestrel()` | Server registration (standalone, not Kestrel) | [Server API](./server) | +| `UseTurboHttp()` | Server registration on `builder.Host` (standalone HTTP server) | [Server API](./server) | | `TurboServerOptions` | Endpoints, protocols, timeouts | [Server API](./server) | | `Http1ServerOptions` / `Http2ServerOptions` / `Http3ServerOptions` | Per-protocol tuning | [Server API](./server) | -| `MapTurboGet/Post/Put/Delete/Patch()` | Route registration | [Server API](./server) | -| `ITurboMiddleware` | Middleware pipeline | [Server API](./server) | -| `TurboHttpContext` | Request/response context with Akka.Streams access | [Server API](./server) | -| `TurboEntityBuilder` | Actor-based entity routing | [Entity Gateway API](./entity-gateway) | +| `app.MapGet/Post/Put/Delete/Patch()` | Standard ASP.NET Core route registration | [Server API](./server) | +| ASP.NET Core middleware | Standard middleware pipeline | [Server API](./server) | +| Standard `HttpContext` | Request/response context via `IFeatureCollection` | [Server API](./server) | ## DI Registration @@ -48,13 +47,15 @@ builder.Services.AddTurboHttpClient(options => { ... }); ### Server ```csharp -builder.Services.AddTurboKestrel(options => +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); }); var app = builder.Build(); -app.Run(); +await app.RunAsync(); ``` ## Quick Links diff --git a/docs/api/server.md b/docs/api/server.md index 0b3d0bf8e..42ed24194 100644 --- a/docs/api/server.md +++ b/docs/api/server.md @@ -1,19 +1,14 @@ # Server API -TurboHTTP Server is a standalone HTTP server built on Akka.Streams. Despite the `AddTurboKestrel` method name (kept for configuration familiarity), it uses its own transport layer via Servus.Akka.Transport. +TurboHTTP Server is an `IServer` implementation for ASP.NET Core built on Akka.Streams. It replaces Kestrel as the transport layer. ## Registration ```csharp -public static class TurboServerServiceCollectionExtensions +public static class TurboServerWebHostBuilderExtensions { - IServiceCollection AddTurboKestrel( - this IServiceCollection services, - Action? configure = null); - - IServiceCollection AddTurboKestrel( - this IServiceCollection services, - IConfiguration configuration, + IHostBuilder UseTurboHttp( + this IHostBuilder builder, Action? configure = null); } ``` @@ -23,395 +18,205 @@ Register the server during application setup: ```csharp var builder = WebApplication.CreateBuilder(args); -// Simple registration -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); }); -// Configuration-driven registration -builder.Services.AddTurboKestrel( - builder.Configuration.GetSection("Turbo"), - options => { /* optional overrides */ }); - var app = builder.Build(); -app.Run(); +app.MapGet("/", () => "Hello from TurboHTTP!"); +await app.RunAsync(); ``` --- -## Server Options - -```csharp -public sealed class TurboServerOptions -{ - public int MaxConcurrentConnections { get; set; } - public int MaxConcurrentUpgradedConnections { get; set; } - public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(120); - public TimeSpan RequestHeadersTimeout { get; set; } = TimeSpan.FromSeconds(30); - public TimeSpan GracefulShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30); - public int BodyBufferThreshold { get; set; } = 65536; - public TimeSpan BodyConsumptionTimeout { get; set; } = TimeSpan.FromSeconds(30); - public int ResponseBodyChunkSize { get; set; } = 16384; - - public Http1ServerOptions Http1 { get; } - public Http2ServerOptions Http2 { get; } - public Http3ServerOptions Http3 { get; } - - void Listen(IPAddress address, ushort port); - void Listen(IPAddress address, ushort port, Action configure); - void ListenLocalhost(ushort port); - void ListenLocalhost(ushort port, Action configure); - void ListenAnyIP(ushort port); - void ListenAnyIP(ushort port, Action configure); -} -``` - -### General Options - -| Property | Default | Description | -|----------|---------|-------------| -| `MaxConcurrentConnections` | System-dependent | Max TCP/QUIC connections at once | -| `MaxConcurrentUpgradedConnections` | System-dependent | Max upgraded connections (e.g., WebSocket) | -| `KeepAliveTimeout` | `120 s` | How long to keep idle connections alive | -| `RequestHeadersTimeout` | `30 s` | Max time to receive complete request headers | -| `GracefulShutdownTimeout` | `30 s` | Max time to drain in-flight requests on shutdown | -| `BodyBufferThreshold` | `65536` (64 KiB) | Buffer limit before switching to streaming | -| `BodyConsumptionTimeout` | `30 s` | Max time to consume request body | -| `ResponseBodyChunkSize` | `16384` (16 KiB) | Chunk size when writing response bodies | - -### Listening Configuration - -Use helper methods to configure endpoints: +## TurboServer ```csharp -options.ListenLocalhost(5100); -options.ListenAnyIP(5100); -options.Listen(IPAddress.Parse("192.168.1.10"), 5100); - -// With TLS configuration -options.ListenLocalhost(5443, listen => +public sealed class TurboServer : IServer { - listen.Certificates.Add( - X509CertificateLoader.LoadPkcs12FromFile("cert.pfx", password)); -}); -``` + IFeatureCollection Features { get; } ---- - -## HTTP/1.x Options + Task StartAsync( + IHttpApplication application, + CancellationToken cancellationToken) where TContext : notnull; -```csharp -public sealed class Http1ServerOptions -{ - public int MaxRequestLineLength { get; set; } = 8192; - public int MaxRequestTargetLength { get; set; } = 8192; - public int MaxPipelinedRequests { get; set; } = 16; - public int MaxChunkExtensionLength { get; set; } = 4096; - public TimeSpan BodyReadTimeout { get; set; } = TimeSpan.FromSeconds(30); + Task StopAsync(CancellationToken cancellationToken); } ``` -| Property | Default | Description | -|----------|---------|-------------| -| `MaxRequestLineLength` | `8192` | Max length of request line (method + URI + version) | -| `MaxRequestTargetLength` | `8192` | Max length of request target URI | -| `MaxPipelinedRequests` | `16` | Max concurrent pipelined requests per connection | -| `MaxChunkExtensionLength` | `4096` | Max chunk extension bytes in chunked transfer encoding | -| `BodyReadTimeout` | `30 s` | Max time to read request body | +`TurboServer` implements `IServer` from `Microsoft.AspNetCore.Hosting.Server`. It creates or reuses an `ActorSystem`, materializes the Akka.Streams pipeline, and spawns the actor hierarchy for connection management. --- -## HTTP/2 Options +## Server Options ```csharp -public sealed class Http2ServerOptions +public sealed class TurboServerOptions { - public int MaxConcurrentStreams { get; set; } = 100; - public int InitialWindowSize { get; set; } = 65535; - public int MaxFrameSize { get; set; } = 16384; - public int MaxHeaderListSize { get; set; } = 8192; - public long MaxRequestBodySize { get; set; } = 30 * 1024 * 1024; - public long MaxResponseBufferSize { get; set; } = 1024 * 1024; - public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(130); - public TimeSpan RequestHeadersTimeout { get; set; } = TimeSpan.FromSeconds(30); - public int MinRequestBodyDataRate { get; set; } = 240; - public TimeSpan MinRequestBodyDataRateGracePeriod { get; set; } = TimeSpan.FromSeconds(5); -} -``` + TurboServerLimits Limits { get; } -| Property | Default | Description | -|----------|---------|-------------| -| `MaxConcurrentStreams` | `100` | Max concurrent streams per connection | -| `InitialWindowSize` | `65535` | Per-stream flow control window | -| `MaxFrameSize` | `16384` (16 KiB) | Max frame payload size | -| `MaxHeaderListSize` | `8192` | Max decompressed header block size | -| `MaxRequestBodySize` | `30 * 1024 * 1024` (30 MB) | Max request body size before rejection | -| `MaxResponseBufferSize` | `1024 * 1024` (1 MB) | Max buffered response data per stream | -| `KeepAliveTimeout` | `130 s` | Idle timeout before PING | -| `RequestHeadersTimeout` | `30 s` | Max time to receive complete headers | -| `MinRequestBodyDataRate` | `240` (bytes/sec) | Minimum acceptable upload rate | -| `MinRequestBodyDataRateGracePeriod` | `5 s` | Grace period before enforcing min rate | + TimeSpan GracefulShutdownTimeout { get; set; } // default: 30s + TimeSpan HandlerTimeout { get; set; } // default: 30s + TimeSpan HandlerGracePeriod { get; set; } // default: 5s ---- + int BodyBufferThreshold { get; set; } // default: 64 * 1024 + TimeSpan BodyConsumptionTimeout { get; set; } // default: 30s + int ResponseBodyChunkSize { get; set; } // default: 16 * 1024 -## HTTP/3 Options + Http1ServerOptions Http1 { get; } + Http2ServerOptions Http2 { get; } + Http3ServerOptions Http3 { get; } -```csharp -public sealed class Http3ServerOptions -{ - public int MaxConcurrentStreams { get; set; } = 100; - public int MaxHeaderListSize { get; set; } = 8192; - public bool EnableWebTransport { get; set; } - public long MaxRequestBodySize { get; set; } = 30 * 1024 * 1024; - public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(130); - public TimeSpan RequestHeadersTimeout { get; set; } = TimeSpan.FromSeconds(30); - public int MinRequestBodyDataRate { get; set; } = 240; - public TimeSpan MinRequestBodyDataRateGracePeriod { get; set; } = TimeSpan.FromSeconds(5); + void Listen(IPAddress address, ushort port); + void Listen(IPAddress address, ushort port, Action configure); + void Listen(string url); + void Listen(string url, Action configure); + void ListenLocalhost(ushort port); + void ListenLocalhost(ushort port, Action configure); + void ListenAnyIP(ushort port); + void ListenAnyIP(ushort port, Action configure); + void Bind(TcpListenerOptions options); + void Bind(QuicListenerOptions options); + void Bind(ListenerOptions options, IListenerFactory factory); + void ConfigureHttpsDefaults(Action configure); + void ConfigureEndpointDefaults(Action configure); } ``` -| Property | Default | Description | -|----------|---------|-------------| -| `MaxConcurrentStreams` | `100` | Max concurrent streams per connection | -| `MaxHeaderListSize` | `8192` | Max decompressed header block size | -| `EnableWebTransport` | `false` | Allow QUIC WebTransport protocol | -| `MaxRequestBodySize` | `30 * 1024 * 1024` (30 MB) | Max request body size before rejection | -| `KeepAliveTimeout` | `130 s` | Idle timeout before PING | -| `RequestHeadersTimeout` | `30 s` | Max time to receive complete headers | -| `MinRequestBodyDataRate` | `240` (bytes/sec) | Minimum acceptable upload rate | -| `MinRequestBodyDataRateGracePeriod` | `5 s` | Grace period before enforcing min rate | - --- -## Routing Extensions - -Extension methods on `WebApplication`: +## Server Limits ```csharp -public static class TurboRoutingExtensions +public sealed class TurboServerLimits { - TurboRouteHandlerBuilder MapTurboGet( - this WebApplication app, string pattern, Delegate handler); - - TurboRouteHandlerBuilder MapTurboPost( - this WebApplication app, string pattern, Delegate handler); - - TurboRouteHandlerBuilder MapTurboPut( - this WebApplication app, string pattern, Delegate handler); - - TurboRouteHandlerBuilder MapTurboDelete( - this WebApplication app, string pattern, Delegate handler); - - TurboRouteHandlerBuilder MapTurboPatch( - this WebApplication app, string pattern, Delegate handler); - - TurboRouteHandlerBuilder MapTurboMethods( - this WebApplication app, string pattern, IEnumerable methods, Delegate handler); - - TurboRouteGroupBuilder MapTurboGroup(this WebApplication app, string prefix); - - TurboRouteHandlerBuilder MapTurboEntity( - this WebApplication app, string pattern, Action configure); - - TurboRouteHandlerBuilder MapTurboEntity( - this WebApplication app, string pattern, Action configure); + int MaxConcurrentConnections { get; set; } // default: 0 (unlimited) + int MaxConcurrentUpgradedConnections { get; set; } // default: 0 (unlimited) + long MaxRequestBodySize { get; set; } // default: 30 * 1024 * 1024 + int MaxRequestHeaderCount { get; set; } // default: 100 + int MaxRequestHeadersTotalSize { get; set; } // default: 32 * 1024 + TimeSpan KeepAliveTimeout { get; set; } // default: 130s + TimeSpan RequestHeadersTimeout { get; set; } // default: 30s + double MinRequestBodyDataRate { get; set; } // default: 0 + TimeSpan MinRequestBodyDataRateGracePeriod { get; set; } // default: 5s + double MinResponseDataRate { get; set; } // default: 0 + TimeSpan MinResponseDataRateGracePeriod { get; set; } // default: 5s } ``` -Basic routing: - -```csharp -app.MapTurboGet("/users/{id}", async (int id, TurboHttpContext context) => -{ - return Results.Ok(new { Id = id }); -}); - -app.MapTurboPost("/users", async (TurboHttpContext context) => -{ - var body = await context.Request.BodyReader.ReadAsync(); - return Results.Created("/users/123", null); -}); - -// Custom methods -app.MapTurboMethods("/events", [HttpMethod.Patch, HttpMethod.Delete], handler); -``` - --- -## Route Handler Builder +## Listen Options ```csharp -public sealed class TurboRouteHandlerBuilder +public sealed class TurboListenOptions(IPAddress address, ushort port) { - TurboRouteHandlerBuilder WithName(string name); - TurboRouteHandlerBuilder WithTags(params string[] tags); - TurboRouteHandlerBuilder WithMetadata(params object[] metadata); - TurboRouteHandlerBuilder RequireAuthorization(); - TurboRouteHandlerBuilder AllowAnonymous(); - TurboRouteHandlerBuilder Produces(int statusCode = 200); - TurboRouteHandlerBuilder ProducesProblem(int statusCode = 500); -} -``` - -Chain builder methods for metadata and OpenAPI documentation: + IPAddress Address { get; } + ushort Port { get; } + HttpProtocols Protocols { get; set; } // default: Http1AndHttp2 -```csharp -app.MapTurboGet("/users/{id}", handler) - .WithName("GetUser") - .WithTags("users") - .Produces(200) - .ProducesProblem(404) - .RequireAuthorization(); + void UseHttps(); + void UseHttps(X509Certificate2 certificate); + void UseHttps(string path, string? password = null); + void UseHttps(Action configure); + void UseHttps(X509Certificate2 certificate, Action configure); + void UseHttps(string path, string? password, Action configure); + void UseConnectionLogging(); + void UseConnectionLogging(string loggerName); +} ``` --- -## Route Group Builder - -Groups routes under a common prefix. Methods inside the group **do not** include the `Turbo` prefix: +## HTTPS Options ```csharp -public sealed class TurboRouteGroupBuilder +public sealed class TurboHttpsOptions { - TurboRouteHandlerBuilder MapGet(string pattern, Delegate handler); - TurboRouteHandlerBuilder MapPost(string pattern, Delegate handler); - TurboRouteHandlerBuilder MapPut(string pattern, Delegate handler); - TurboRouteHandlerBuilder MapDelete(string pattern, Delegate handler); - TurboRouteHandlerBuilder MapPatch(string pattern, Delegate handler); - TurboRouteHandlerBuilder MapMethods(string pattern, IEnumerable methods, Delegate handler); - TurboRouteGroupBuilder MapGroup(string prefix); - TurboRouteHandlerBuilder MapEntity(string pattern, Action configure); - TurboRouteHandlerBuilder MapEntity(string pattern, Action configure); + X509Certificate2? ServerCertificate { get; set; } + string? CertificatePath { get; set; } + string? CertificatePassword { get; set; } + SslProtocols EnabledSslProtocols { get; set; } // default: None (OS default) + RemoteCertificateValidationCallback? ClientCertificateValidationCallback { get; set; } + TimeSpan HandshakeTimeout { get; set; } // default: 10s + ClientCertificateMode ClientCertificateMode { get; set; } // default: NoCertificate + Func? ServerCertificateSelector { get; set; } } ``` -Example: - -```csharp -var api = app.MapTurboGroup("/api"); - -api.MapGet("/users", listUsers); -api.MapPost("/users", createUser); -api.MapGet("/users/{id}", getUser); - -// Nested groups -var v2 = api.MapGroup("/v2"); -v2.MapGet("/status", status); -``` - --- -## Middleware - -Middleware components implement `ITurboMiddleware`: +## HTTP Protocols ```csharp -public interface ITurboMiddleware +[Flags] +public enum HttpProtocols { - Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next); + None = 0, + Http1 = 1, + Http2 = 2, + Http1AndHttp2 = Http1 | Http2, + Http3 = 4 } - -public delegate Task TurboRequestDelegate(TurboHttpContext context); ``` -Register middleware via `UseTurbo`: - -```csharp -// Built-in middleware -public static class TurboMiddlewareExtensions -{ - WebApplication UseTurbo(this WebApplication app) where T : ITurboMiddleware; - WebApplication UseTurbo(this WebApplication app, Func middleware); -} -``` +--- -Example middleware: +## HTTP/1.x Options ```csharp -public class RequestLoggingMiddleware : ITurboMiddleware +public sealed class Http1ServerOptions { - private readonly ILogger _logger; - - public RequestLoggingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - _logger.LogInformation("{Method} {Path}", - context.Request.Method, context.Request.Path); - - await next(context); - - _logger.LogInformation("Response: {StatusCode}", context.Response.StatusCode); - } + int MaxRequestLineLength { get; set; } // default: 8192 + int MaxRequestTargetLength { get; set; } // default: 8192 + int MaxPipelinedRequests { get; set; } // default: 16 + int MaxChunkExtensionLength { get; set; } // default: 4096 + TimeSpan BodyReadTimeout { get; set; } // default: 30s + long MaxRequestBodySize { get; set; } // default: 30_000_000 + int MaxHeaderListSize { get; set; } // default: 32 * 1024 + TimeSpan? KeepAliveTimeout { get; set; } // default: null (uses global) + TimeSpan? RequestHeadersTimeout { get; set; } // default: null (uses global) } - -// Register it -app.UseTurbo(); - -// Or inline -app.UseTurbo(async (context, next) => -{ - Console.WriteLine($"{context.Request.Method} {context.Request.Path}"); - await next(context); -}); ``` --- -## TurboHttpContext - -`TurboHttpContext` extends ASP.NET Core's `HttpContext` and adds Akka.Streams-specific properties: +## HTTP/2 Options ```csharp -public sealed class TurboHttpContext : HttpContext +public sealed class Http2ServerOptions { - public TurboHttpRequest TurboRequest { get; } - public TurboHttpResponse TurboResponse { get; } - public IMaterializer Materializer { get; } + int MaxConcurrentStreams { get; set; } // default: 100 + int InitialConnectionWindowSize { get; set; } // default: 1 * 1024 * 1024 + int InitialStreamWindowSize { get; set; } // default: 768 * 1024 + int MaxFrameSize { get; set; } // default: 16 * 1024 + int MaxHeaderListSize { get; set; } // default: 32 * 1024 + int HeaderTableSize { get; set; } // default: 4 * 1024 + long MaxRequestBodySize { get; set; } // default: 30_000_000 + long MaxResponseBufferSize { get; set; } // default: 64 * 1024 + TimeSpan KeepAliveTimeout { get; set; } // default: 130s + TimeSpan RequestHeadersTimeout { get; set; } // default: 30s + int MinRequestBodyDataRate { get; set; } // default: 240 + TimeSpan MinRequestBodyDataRateGracePeriod { get; set; } // default: 5s } ``` -### TurboRequest +--- -Provides stream-based request body access: +## HTTP/3 Options ```csharp -// Access the request as a byte stream -var reader = context.TurboRequest.BodySource; -await foreach (var bytes in reader.ReadAllAsync(cancellationToken)) +public sealed class Http3ServerOptions { - // Process streaming body chunks + int MaxConcurrentStreams { get; set; } // default: 100 + int MaxHeaderListSize { get; set; } // default: 32 * 1024 + int QpackMaxTableCapacity { get; set; } // default: 0 + bool EnableWebTransport { get; set; } // default: false + long MaxRequestBodySize { get; set; } // default: 30_000_000 + TimeSpan KeepAliveTimeout { get; set; } // default: 130s + TimeSpan RequestHeadersTimeout { get; set; } // default: 30s + int MinRequestBodyDataRate { get; set; } // default: 240 + TimeSpan MinRequestBodyDataRateGracePeriod { get; set; } // default: 5s } ``` - -### TurboResponse - -Provides stream-based response body writing: - -```csharp -context.Response.StatusCode = 200; -context.Response.ContentType = "text/plain"; - -var writer = context.TurboResponse.BodyWriter; -await writer.WriteAsync(new Memory(utf8Bytes), cancellationToken); -await writer.CompleteAsync(); -``` - -### Materializer - -The Akka.Streams materializer running in the context of this request. Useful for running stream graphs: - -```csharp -var source = Source.From(items); -var sink = Sink.Aggregate>(new List(), (list, item) => -{ - list.Add(item); - return list; -}); - -var result = await source.RunWith(sink, context.Materializer); -``` diff --git a/docs/architecture/handlers.md b/docs/architecture/handlers.md index 131b93ddd..5d029bf48 100644 --- a/docs/architecture/handlers.md +++ b/docs/architecture/handlers.md @@ -310,9 +310,9 @@ On the server side, incoming requests flow through a different pipeline. Each `C -Network bytes arrive at the protocol-specific `ConnectionStage`, are decoded into HTTP requests, wrapped in a `TurboHttpContext` by the `HttpContextBidiStage`, pass through the middleware pipeline, and reach the routing stage which dispatches to the matched handler. +Network bytes arrive at the protocol-specific `ConnectionStage`, are decoded into HTTP requests, wrapped in an `IFeatureCollection` (standard `HttpContext`) by the `ApplicationBridgeStage`, pass through the middleware pipeline, and reach the routing stage which dispatches to the matched handler. ## Related Guides -- [Middleware Pipeline](/server/middleware) — server middleware composition +- [ASP.NET Core Integration](/server/aspnet-core) — server middleware composition - [Feature Options](/api/feature-options) — client pipeline extensions diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 611422ce5..b4879d93c 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -72,11 +72,9 @@ Incoming TCP/QUIC Connection ↓ [Protocol Decoder] — parses HTTP/1.0, 1.1, 2, or 3 bytes ↓ -[HttpContext Builder] — creates TurboHttpContext from parsed request +[ApplicationBridgeStage] — bridges IFeatureCollection to ASP.NET Core ↓ -[Middleware Pipeline] — runs registered middleware (Use/Run/Map/MapWhen) - ↓ -[Router] — matches request to registered route +[ASP.NET Core Pipeline] — middleware, routing, handler execution ↓ [Dispatcher] — DelegateDispatcher (handler) or EntityDispatcher (actor) ↓ diff --git a/docs/architecture/layers.md b/docs/architecture/layers.md index 3579c1815..0685f3b0e 100644 --- a/docs/architecture/layers.md +++ b/docs/architecture/layers.md @@ -79,12 +79,12 @@ TurboHTTP Server provides an ASP.NET Core-style programming model for handling i ### Registration -Register the server via `AddTurboKestrel` and configure endpoints: +Register the server via `UseTurboHttp` on `builder.Host` and configure endpoints: ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); options.ListenLocalhost(5101, listen => listen.UseHttps()); @@ -95,10 +95,10 @@ var app = builder.Build(); ### Middleware -Register middleware and routes on the `WebApplication` instance: +Register middleware and routes on the `WebApplication` instance using standard ASP.NET Core methods: ```csharp -app.UseTurbo(async (context, next) => +app.Use(async (context, next) => { // process request await next(context); @@ -108,24 +108,9 @@ app.UseTurbo(async (context, next) => ### Routes -Define routes with Minimal API-style methods: +Define routes with standard ASP.NET Core Minimal API methods: ```csharp -app.MapTurboGet("/users/{id:int}", (int id) => GetUser(id)); -app.MapTurboPost("/users", (CreateUserRequest req) => Results.Created($"/users/{req.Id}", req)); -``` - -### Entity Gateway - -Route to Akka.NET actors for stateful request handling: - -```csharp -app.MapTurboEntity("/orders/{id:int}", entity => -{ - entity.UseActorRef(); - entity.OnGet((int id) => new GetOrder(id)); - entity.OnPost((int id, CreateOrderRequest req) => new CreateOrder(id, req.Items)); - entity.OnPut((int id, UpdateOrderRequest req) => new UpdateOrder(id, req.Status)); - entity.OnDelete((int id) => new CancelOrder(id)); -}); +app.MapGet("/users/{id:int}", (int id) => GetUser(id)); +app.MapPost("/users", (CreateUserRequest req) => Results.Created($"/users/{req.Id}", req)); ``` diff --git a/docs/architecture/pipeline.md b/docs/architecture/pipeline.md index 12aa6451b..97d7f1e06 100644 --- a/docs/architecture/pipeline.md +++ b/docs/architecture/pipeline.md @@ -104,13 +104,13 @@ Incoming TCP/QUIC Bytes ↓ [Server Protocol Engine] — Http10/11/20/30ServerEngine decodes request, encodes response ↓ -[HttpContextBidiStage] — wraps parsed request as TurboHttpContext (request/response object) +[ApplicationBridgeStage] — wraps parsed request as IFeatureCollection (HttpContext) ↓ -[MiddlewarePipelineStage] — runs registered middleware (Use/Run/Map/MapWhen) +[Middleware] — runs registered middleware (Use/Run/Map/MapWhen) ↓ -[RoutingStage] — matches request path to registered route pattern +[Routing] — matches request path to registered route pattern ↓ -[DispatcherStage] — delegates to DelegateDispatcher (handler function) or EntityDispatcher (actor) +[Dispatcher] — delegates to handler function or actor ↓ [Handler / Entity Actor] — executes your code; returns response ↓ @@ -127,10 +127,10 @@ Each connection is bound to a single `ConnectionActor` that owns the entire Akka | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ProtocolRouter` | Inspects initial bytes to detect HTTP/1.0, 1.1, 2, or 3; routes to the appropriate server engine state machine | | `Http*ServerEngine` | Protocol-specific state machine: parses request bytes, manages connection/stream-level flow control, encodes response frames | -| `HttpContextBidiStage` | Wraps the parsed protocol request as a `TurboHttpContext` object with `.Request` and `.Response` properties | -| `MiddlewarePipelineStage` | Runs all registered middleware in order (outermost-first for request, innermost-first for response). Middleware can short-circuit by not calling `next(ctx)` | -| `RoutingStage` | Matches the request path against registered route patterns; extracts route parameters (`{id}`, etc.) into `ctx.RouteValues` | -| `DispatcherStage` | Selects and invokes the handler: `DelegateDispatcher` for function-based routes (`MapTurboGet`), `EntityDispatcher` for actor-based routes (`MapTurboEntity`) | +| `ApplicationBridgeStage` | Wraps the parsed protocol request as an `IFeatureCollection` (standard ASP.NET Core `HttpContext`) | +| Middleware | Runs all registered middleware in order (outermost-first for request, innermost-first for response). Middleware can short-circuit by not calling `next(ctx)` | +| Routing | Matches the request path against registered route patterns; extracts route parameters (`{id}`, etc.) into route values | +| Dispatcher | Selects and invokes the handler: standard handler functions or actor-based routes | | `ParameterBindingStage` | (within dispatcher) Binds route parameters, query string, body, and headers to handler parameters using reflection and model binding | After the handler returns a response, the response object flows back through the pipeline in reverse — middleware response hooks can transform or log the response, and the protocol engine serialises it back to wire bytes. diff --git a/docs/architecture/server-engines.md b/docs/architecture/server-engines.md index 409a7a2ab..bb7eb185e 100644 --- a/docs/architecture/server-engines.md +++ b/docs/architecture/server-engines.md @@ -97,16 +97,13 @@ After encoding each response, `Http11ServerEngine` evaluates the `Connection` he **Configuration:** ```csharp -var options = new TurboServerOptions +builder.Host.UseTurboHttp(options => { - Http2 = new Http2Options - { - MaxFrameSize = 16 * 1024, // default 16KB - MaxHeaderListSize = 32 * 1024, // default 32KB - InitialWindowSize = 65_535, // stream-level flow control window - InitialConnectionWindowSize = 1 * 1024 * 1024, // connection-level window - } -}; + options.Http2.MaxFrameSize = 16 * 1024; + options.Http2.MaxHeaderListSize = 32 * 1024; + options.Http2.InitialStreamWindowSize = 768 * 1024; + options.Http2.InitialConnectionWindowSize = 1 * 1024 * 1024; +}); ``` --- @@ -139,16 +136,11 @@ var options = new TurboServerOptions **Configuration:** ```csharp -var options = new TurboServerOptions +builder.Host.UseTurboHttp(options => { - Http3 = new Http3Options - { - MaxFrameSize = 16 * 1024, // default 16KB - MaxHeaderListSize = 32 * 1024, // default 32KB - InitialMaxStreamDataBidiLocal = 1 * 1024 * 1024, // per-stream flow control - InitialMaxData = 10 * 1024 * 1024, // connection-level flow control - } -}; + options.Http3.MaxHeaderListSize = 32 * 1024; + options.Http3.QpackMaxTableCapacity = 4 * 1024; +}); ``` --- @@ -158,19 +150,19 @@ var options = new TurboServerOptions Each protocol has its own configuration section on `TurboServerOptions`: ```csharp -var options = new TurboServerOptions +builder.Host.UseTurboHttp(options => { - Binding = new BindingOptions + options.ListenLocalhost(5000); + options.ListenLocalhost(5001, listen => { - Port = 8080, - EnableHttp1 = true, - EnableHttp2 = true, - EnableHttp3 = true, - }, - Http1 = new Http1Options { IdleTimeout = TimeSpan.FromSeconds(120) }, - Http2 = new Http2Options { MaxFrameSize = 32 * 1024 }, - Http3 = new Http3Options { MaxHeaderListSize = 64 * 1024 }, -}; + listen.UseHttps(); + listen.Protocols = HttpProtocols.Http1AndHttp2; + }); + + options.Http1.KeepAliveTimeout = TimeSpan.FromSeconds(120); + options.Http2.MaxFrameSize = 32 * 1024; + options.Http3.MaxHeaderListSize = 64 * 1024; +}); ``` ::: tip @@ -205,5 +197,6 @@ If no ALPN is advertised or negotiation fails, the server defaults to **HTTP/1.1 - [HTTP/2 & Multiplexing](/client/http2) — client-side HTTP/2 configuration - [HTTP/3 & QUIC](/client/http3) — client-side HTTP/3 configuration -- [Server Configuration](/server/configuration) — server protocol settings -- [Hosting & Lifecycle](/server/hosting) — connection management +- [Configuration](/server/configuration) — all server options +- [Hosting & Lifecycle](/server/hosting) — actor hierarchy and shutdown +- [Using with ASP.NET Core](/server/aspnet-core) — how the pipeline integrates diff --git a/docs/architecture/server-pipeline.md b/docs/architecture/server-pipeline.md index c0a4e7c0b..fe05a8048 100644 --- a/docs/architecture/server-pipeline.md +++ b/docs/architecture/server-pipeline.md @@ -21,13 +21,13 @@ Incoming TCP/QUIC Connection ↓ [Protocol Decoder] — Http10/11/20/30ServerEngine decodes request ↓ -[HttpContextBidiStage] — wraps parsed request as TurboHttpContext +[ApplicationBridgeStage] — wraps parsed request as IFeatureCollection (HttpContext) ↓ -[MiddlewarePipelineStage] — runs registered middleware (Use/Run/Map/MapWhen) +[Middleware] — runs registered middleware (Use/Run/Map/MapWhen) ↓ -[RoutingStage] — matches request path to registered route pattern +[Routing] — matches request path to registered route pattern ↓ -[DispatcherStage] — delegates to DelegateDispatcher or EntityDispatcher +[Dispatcher] — delegates to handler function or actor ↓ [Parameter Binding] — binds route values, query, body, headers to handler parameters ↓ @@ -47,10 +47,10 @@ Outgoing TCP/QUIC Bytes | `Transport` (TCP/QUIC) | `ListenerActor` binds transport, accepts incoming connections, spawns `ConnectionActor` per client | | `ProtocolRouter` | Inspects initial bytes to detect HTTP version; routes to appropriate server engine state machine | | `Http*ServerEngine` | Protocol-specific state machine: parses request bytes, manages connection/stream-level flow control, encodes response frames | -| `HttpContextBidiStage` | Wraps the parsed protocol request as a `TurboHttpContext` object with `.Request` and `.Response` properties | -| `MiddlewarePipelineStage` | Runs all registered middleware in order (outermost-first for request, innermost-first for response). Middleware can short-circuit. | -| `RoutingStage` | Matches the request path against registered route patterns; extracts route parameters (`{id}`, etc.) into `ctx.RouteValues` | -| `DispatcherStage` | Selects and invokes the handler: `DelegateDispatcher` for function-based routes, `EntityDispatcher` for actor-based routes | +| `ApplicationBridgeStage` | Wraps the parsed protocol request as an `IFeatureCollection` (standard ASP.NET Core `HttpContext`) | +| Middleware | Runs all registered middleware in order (outermost-first for request, innermost-first for response). Middleware can short-circuit. | +| Routing | Matches the request path against registered route patterns; extracts route parameters (`{id}`, etc.) into route values | +| Dispatcher | Selects and invokes the handler: function-based routes or actor-based routes | | `ParameterBindingStage` | (within dispatcher) Binds route parameters, query string, body, and headers to handler parameters using reflection and model binding | --- @@ -118,29 +118,13 @@ Unlike ASP.NET Core where middleware is registered in reverse order, TurboHTTP m --- -## Routing & Parameter Binding +## ASP.NET Core Integration -`RoutingStage` matches request paths to registered route patterns: - -```csharp -app.MapTurboGet("/users/{id}", handler); -app.MapTurboPost("/users", handler); -app.MapTurboDelete("/users/{id}", handler); -``` - -On a match, `ParameterBindingStage` extracts: -- **Route parameters** — `{id}` from the matched route pattern -- **Query string** — `?sort=name&limit=10` -- **Request body** — JSON/form-encoded data -- **Headers** — `Authorization`, `X-Custom-Header`, etc. - -These are bound to handler method parameters using reflection and model binding. +After `ApplicationBridgeStage` creates the `TContext` from the `IFeatureCollection`, ASP.NET Core's standard middleware pipeline takes over — routing, model binding, authentication, and handler execution are all handled by ASP.NET Core, not by TurboHTTP. --- ## Related Guides -- [Middleware Pipeline](/server/middleware) — configure middleware -- [Routing](/server/routing) — route registration and parameter binding -- [Entity Gateway](/server/entity-gateway) — actor-based entity handling +- [ASP.NET Core Integration](/server/aspnet-core) — middleware, routing, and request handling - [Hosting & Lifecycle](/server/hosting) — actor hierarchy and graceful shutdown diff --git a/docs/getting-started/architecture.md b/docs/getting-started/architecture.md index 4b05c7069..12577c9da 100644 --- a/docs/getting-started/architecture.md +++ b/docs/getting-started/architecture.md @@ -99,13 +99,13 @@ Incoming TCP/QUIC Connection ↓ [Protocol Decoder] — parses HTTP/1.0, 1.1, 2, or 3 bytes ↓ -[HttpContext Builder] — creates TurboHttpContext from parsed request +[HttpContext Builder] — creates standard HttpContext from parsed request ↓ [Middleware Pipeline] — runs registered middleware (Use/Run/Map/MapWhen) ↓ [Router] — matches request to registered route ↓ -[Dispatcher] — DelegateDispatcher (handler) or EntityDispatcher (actor) +[Dispatcher] — handler function or actor ↓ [Parameter Binding] — binds route values, query, body, headers to handler parameters ↓ diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 64808c43b..c38e18be0 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -54,21 +54,21 @@ var response = await client.SendAsync( ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); }); var app = builder.Build(); -app.MapTurboGet("/health", () => new { status = "healthy" }); -app.MapTurboGet("/users/{id}", (int id) => new { id, name = "User " + id }); +app.MapGet("/health", () => new { status = "healthy" }); +app.MapGet("/users/{id}", (int id) => new { id, name = "User " + id }); await app.RunAsync(); ``` -::: tip About AddTurboKestrel -Despite the name, TurboHTTP Server is a fully standalone HTTP server built on Akka.Streams with its own TCP/QUIC transport layer. The method is named `AddTurboKestrel` for configuration familiarity — it does not use or depend on Kestrel. +::: tip About UseTurboHttp +TurboHTTP Server is a fully standalone HTTP server built on Akka.Streams with its own TCP/QUIC transport layer. Register it on `builder.Host` using `UseTurboHttp()` — it does not use or depend on Kestrel. ::: ## Next Steps diff --git a/docs/getting-started/server.md b/docs/getting-started/server.md index 23d3fe093..d903ea66b 100644 --- a/docs/getting-started/server.md +++ b/docs/getting-started/server.md @@ -1,98 +1,80 @@ # Server Quick Start -Build a working TurboHTTP server in under 5 minutes. +Build a working TurboHTTP server in under 5 minutes. TurboHTTP replaces Kestrel as a drop-in `IServer` implementation — everything above the transport layer is standard ASP.NET Core. -::: tip Standalone Server -TurboHTTP Server is a fully standalone HTTP server built on Akka.Streams with its own TCP/QUIC transport (Servus.Akka.Transport). The `AddTurboKestrel` method name is a configuration convention — it does not use Kestrel. -::: - -## 1. Install +## 1. Create a Project ```bash +dotnet new web -n MyTurboApp +cd MyTurboApp dotnet add package TurboHTTP ``` ## 2. Configure the Server -```csharp -using TurboHTTP.Hosting; +In `Program.cs`: +```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); }); var app = builder.Build(); -``` -## 3. Add Routes +app.MapGet("/health", () => new { status = "healthy" }); -```csharp -app.MapTurboGet("/health", () => new { status = "healthy" }); +app.MapGet("/", () => "Hello from TurboHTTP!"); -app.MapTurboGet("/users/{id}", (int id) => new { id, name = "User " + id }); +await app.RunAsync(); +``` -app.MapTurboPost("/users", (CreateUserRequest req) => - new { created = true, name = req.Name }); +`UseTurboHttp()` registers `TurboServer` as the `IServer` implementation. After that, you use standard ASP.NET Core — `app.MapGet`, `app.UseRouting`, middleware, controllers, minimal APIs — everything works as you'd expect. -app.MapTurboDelete("/users/{id}", (int id) => new { deleted = true, id }); +## 3. Test It -await app.RunAsync(); +```bash +dotnet run + +# In another terminal: +curl http://localhost:5100/health +# {"status":"healthy"} -public sealed record CreateUserRequest(string Name, string Email); +curl http://localhost:5100/ +# Hello from TurboHTTP! ``` -## 4. Add Middleware +## 4. Add HTTPS ```csharp -app.UseTurbo(async (context, next) => +builder.Host.UseTurboHttp(options => { - Console.WriteLine($"[{context.Request.Method}] {context.Request.Path}"); - await next(context); + options.ListenLocalhost(5100); + options.ListenLocalhost(5101, listen => + { + listen.UseHttps(); + listen.Protocols = HttpProtocols.Http1AndHttp2; + }); }); ``` -## 5. Route Groups +## What's Different from Kestrel? -```csharp -var api = app.MapTurboGroup("/api/v1"); -api.MapGet("/users", () => new[] { "Alice", "Bob" }); -api.MapPost("/users", (CreateUserRequest req) => new { created = true }); -``` - -::: warning Route Group Methods -Inside a route group, use `MapGet`, `MapPost`, etc. (no "Turbo" prefix). The `MapTurbo*` prefix is only on `WebApplication` extension methods. -::: - -## 6. Test It - -```bash -curl http://localhost:5100/health -curl http://localhost:5100/users/42 -curl -X POST http://localhost:5100/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}' -``` - -## Server Architecture - -TurboHTTP Server uses an actor hierarchy for connection management: - -``` -ServerSupervisorActor -├── ListenerActor (endpoint :5100) -│ ├── ConnectionActor (client A) -│ └── ConnectionActor (client B) -└── ListenerActor (endpoint :5101) - └── ConnectionActor (client C) -``` +TurboHTTP is a transport-level replacement — it handles TCP/QUIC connections, protocol negotiation, and HTTP wire format. Your ASP.NET Core code stays the same. -Each connection gets its own actor, and protocol engines (HTTP/1.0, 1.1, 2, 3) are selected via ALPN negotiation. +| | Kestrel | TurboHTTP | +|---|---------|-----------| +| Transport | libuv / SocketsHttpHandler | Akka.Streams + Servus.Akka.Transport | +| Connection model | Thread pool | Actor per connection | +| Protocols | HTTP/1.1, HTTP/2, HTTP/3 | HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3 | +| Backpressure | Pipe-based | Akka.Streams reactive streams | +| Shutdown | IHostApplicationLifetime | Akka Coordinated Shutdown | ## Next Steps -- [Installation & Setup](/server/installation) — endpoints, HTTPS, protocols -- [Middleware Pipeline](/server/middleware) — composition, error handling -- [Routing](/server/routing) — parameters, binding, groups -- [Entity Gateway](/server/entity-gateway) — actor-based stateful handling -- [Real-World Scenarios](/server/scenarios) — combined feature examples +- [Installation & Setup](/server/installation) — endpoints, HTTPS, certificates +- [Configuration](/server/configuration) — all server options +- [Using with ASP.NET Core](/server/aspnet-core) — middleware, routing, DI guidance +- [Hosting & Lifecycle](/server/hosting) — actor hierarchy, graceful shutdown diff --git a/docs/likec4/model-pipeline.c4 b/docs/likec4/model-pipeline.c4 index c7a0667e7..8d4307b66 100644 --- a/docs/likec4/model-pipeline.c4 +++ b/docs/likec4/model-pipeline.c4 @@ -18,8 +18,8 @@ // cache hit --- cached response --> response chain at retry level (bypasses ContentEncoding + engine) // // SERVER PIPELINE (per connection, inside ConnectionActor): -// Network -> ConnectionStage -> HttpContextBidiStage -> MiddlewarePipelineStage -> RoutingStage -// Response flows back through the same stages in reverse. +// Network -> ConnectionStage -> ApplicationBridgeStage -> IHttpApplication (ASP.NET Core) +// Response flows back through the same IFeatureCollection. model { // Request chain (outermost -> innermost) @@ -102,27 +102,22 @@ model { network -[flows]-> turbohttp.serverStages.Http20ServerConnectionStage 'inbound bytes' network -[flows]-> turbohttp.serverStages.Http30ServerConnectionStage 'inbound bytes' - // ─── Server: ConnectionStage → HttpContextBidiStage (decoded request) + // ─── Server: ConnectionStage → ApplicationBridgeStage (decoded request as IFeatureCollection) - turbohttp.serverStages.Http10ServerConnectionStage -[flows]-> turbohttp.serverStages.HttpContextBidiStage 'decoded request' - turbohttp.serverStages.Http11ServerConnectionStage -[flows]-> turbohttp.serverStages.HttpContextBidiStage 'decoded request' - turbohttp.serverStages.Http20ServerConnectionStage -[flows]-> turbohttp.serverStages.HttpContextBidiStage 'decoded request' - turbohttp.serverStages.Http30ServerConnectionStage -[flows]-> turbohttp.serverStages.HttpContextBidiStage 'decoded request' + turbohttp.serverStages.Http10ServerConnectionStage -[flows]-> turbohttp.serverStages.ApplicationBridgeStage 'IFeatureCollection' + turbohttp.serverStages.Http11ServerConnectionStage -[flows]-> turbohttp.serverStages.ApplicationBridgeStage 'IFeatureCollection' + turbohttp.serverStages.Http20ServerConnectionStage -[flows]-> turbohttp.serverStages.ApplicationBridgeStage 'IFeatureCollection' + turbohttp.serverStages.Http30ServerConnectionStage -[flows]-> turbohttp.serverStages.ApplicationBridgeStage 'IFeatureCollection' - // ─── Server: Request chain — HttpContext → Middleware → Routing ──── + // ─── Server: ApplicationBridgeStage dispatches to ASP.NET Core ──── + // ApplicationBridgeStage calls IHttpApplication.CreateContext(features) → ProcessRequestAsync() → DisposeContext() - turbohttp.serverStages.HttpContextBidiStage -[flows]-> turbohttp.serverStages.MiddlewarePipelineStage 'TurboHttpContext' - turbohttp.serverStages.MiddlewarePipelineStage -[flows]-> turbohttp.serverStages.RoutingStage 'request (middleware applied)' + // ─── Server: Response — ApplicationBridgeStage → ConnectionStage → Network - // ─── Server: Response chain — Routing → Middleware → HttpContext → ConnectionStage - - turbohttp.serverStages.RoutingStage -[flows]-> turbohttp.serverStages.MiddlewarePipelineStage 'response' - turbohttp.serverStages.MiddlewarePipelineStage -[flows]-> turbohttp.serverStages.HttpContextBidiStage 'response (after middleware)' - - turbohttp.serverStages.HttpContextBidiStage -[flows]-> turbohttp.serverStages.Http10ServerConnectionStage 'response' - turbohttp.serverStages.HttpContextBidiStage -[flows]-> turbohttp.serverStages.Http11ServerConnectionStage 'response' - turbohttp.serverStages.HttpContextBidiStage -[flows]-> turbohttp.serverStages.Http20ServerConnectionStage 'response' - turbohttp.serverStages.HttpContextBidiStage -[flows]-> turbohttp.serverStages.Http30ServerConnectionStage 'response' + turbohttp.serverStages.ApplicationBridgeStage -[flows]-> turbohttp.serverStages.Http10ServerConnectionStage 'response IFeatureCollection' + turbohttp.serverStages.ApplicationBridgeStage -[flows]-> turbohttp.serverStages.Http11ServerConnectionStage 'response IFeatureCollection' + turbohttp.serverStages.ApplicationBridgeStage -[flows]-> turbohttp.serverStages.Http20ServerConnectionStage 'response IFeatureCollection' + turbohttp.serverStages.ApplicationBridgeStage -[flows]-> turbohttp.serverStages.Http30ServerConnectionStage 'response IFeatureCollection' // ─── Server: Outbound — ConnectionStage → Network (wire encode) ─── diff --git a/docs/likec4/model.c4 b/docs/likec4/model.c4 index b8b5c5ca2..7adf51468 100644 --- a/docs/likec4/model.c4 +++ b/docs/likec4/model.c4 @@ -181,13 +181,13 @@ model { server = container 'Server Layer' { #server - description 'Standalone HTTP server with actor-based lifecycle, middleware, and routing' + description 'IServer implementation with actor-based lifecycle and Akka.Streams pipeline' technology 'C# / Akka.NET / Servus.Akka.Transport' - TurboServerHostedService = component 'TurboServerHostedService' { - #server - technology 'IHostedService' - description 'ASP.NET Core hosted service entry point: creates ActorSystem, materializer, and spawns ServerSupervisorActor on startup' + TurboServer = component 'TurboServer' { + #server #api + technology 'IServer' + description 'ASP.NET Core IServer implementation: creates ActorSystem, materializer, ApplicationBridgeStage, and spawns ServerSupervisorActor on startup' } ServerSupervisorActor = actor 'ServerSupervisorActor' { @@ -282,22 +282,10 @@ model { description 'Server-side HTTP/3: QPACK decompression, frame decoding over QUIC streams, flow control, GOAWAY, response encoding' } - HttpContextBidiStage = component 'HttpContextBidiStage' { - #server #stage - technology 'BidiStage' - description 'Creates TurboHttpContext from decoded HTTP requests; writes response back to the protocol layer' - } - - MiddlewarePipelineStage = component 'MiddlewarePipelineStage' { - #server #stage - technology 'GraphStage' - description 'Executes the middleware chain (Use/Run/Map/MapWhen) for each request; middleware can short-circuit or delegate to next' - } - - RoutingStage = component 'RoutingStage' { + ApplicationBridgeStage = component 'ApplicationBridgeStage' { #server #stage - technology 'GraphStage' - description 'Matches incoming requests to registered route handlers (MapTurboGet/Post/Put/Delete/Patch); dispatches to handler delegates or entity gateway' + technology 'GraphStage' + description 'Bridges Akka.Streams to ASP.NET Core: creates TContext from IFeatureCollection via IHttpApplication.CreateContext(), dispatches to ProcessRequestAsync(), emits response features' } } } @@ -374,8 +362,8 @@ model { turbohttp.streams.Http20ClientEngine -> servus.TcpConnectionStage 'TCP transport' turbohttp.streams.Http30ClientEngine -> servus.QuicConnectionStage 'QUIC transport' - turbohttp.server.TurboServerHostedService -> turbohttp.server.ServerSupervisorActor 'Creates on startup' - turbohttp.server.TurboServerHostedService -> turbohttp.server.TurboServerOptions 'Reads configuration' + turbohttp.server.TurboServer -> turbohttp.server.ServerSupervisorActor 'Creates on startup' + turbohttp.server.TurboServer -> turbohttp.server.TurboServerOptions 'Reads configuration' turbohttp.server.ServerSupervisorActor -> turbohttp.server.ListenerActor 'Spawns per endpoint' turbohttp.server.ListenerActor -> turbohttp.server.ConnectionActor 'Spawns per client' @@ -385,11 +373,8 @@ model { servus.QuicListenerFactory -> network 'Accepts QUIC connections' turbohttp.server.ConnectionActor -> turbohttp.serverStages.NegotiatingServerEngine 'Materialises protocol engine' - turbohttp.server.ConnectionActor -> turbohttp.serverStages.HttpContextBidiStage 'Creates HTTP context' - turbohttp.server.ConnectionActor -> turbohttp.serverStages.MiddlewarePipelineStage 'Runs middleware' - turbohttp.server.ConnectionActor -> turbohttp.serverStages.RoutingStage 'Dispatches to routes' + turbohttp.server.ConnectionActor -> turbohttp.serverStages.ApplicationBridgeStage 'Dispatches to IHttpApplication' - turbohttp.serverStages.NegotiatingServerEngine -> turbohttp.server.ProtocolRouter 'Resolves protocol via ALPN' turbohttp.serverStages.NegotiatingServerEngine -> turbohttp.serverStages.Http10ServerEngine 'HTTP/1.0' turbohttp.serverStages.NegotiatingServerEngine -> turbohttp.serverStages.Http11ServerEngine 'HTTP/1.1' turbohttp.serverStages.NegotiatingServerEngine -> turbohttp.serverStages.Http20ServerEngine 'HTTP/2' diff --git a/docs/likec4/views-architecture.c4 b/docs/likec4/views-architecture.c4 index a5d025160..a9eb4389d 100644 --- a/docs/likec4/views-architecture.c4 +++ b/docs/likec4/views-architecture.c4 @@ -33,9 +33,7 @@ views { include turbohttp.streams.ExpectContinueBidiStage include turbohttp.streams.AltSvcBidiStage include turbohttp.serverStages.NegotiatingServerEngine - include turbohttp.serverStages.HttpContextBidiStage - include turbohttp.serverStages.MiddlewarePipelineStage - include turbohttp.serverStages.RoutingStage + include turbohttp.serverStages.ApplicationBridgeStage include network } @@ -93,7 +91,7 @@ views { view serverPipeline of turbohttp.serverStages { title 'Server — Request Pipeline' - description 'How an incoming HTTP request flows through the server: ALPN negotiation → wire decode → context creation → middleware → routing → response' + description 'How an incoming HTTP request flows through the server: ALPN negotiation → wire decode → ApplicationBridgeStage → ASP.NET Core' include turbohttp.server.ConnectionActor include turbohttp.server.ProtocolRouter include turbohttp.serverStages.NegotiatingServerEngine @@ -105,9 +103,7 @@ views { include turbohttp.serverStages.Http11ServerConnectionStage include turbohttp.serverStages.Http20ServerConnectionStage include turbohttp.serverStages.Http30ServerConnectionStage - include turbohttp.serverStages.HttpContextBidiStage - include turbohttp.serverStages.MiddlewarePipelineStage - include turbohttp.serverStages.RoutingStage + include turbohttp.serverStages.ApplicationBridgeStage include network } } diff --git a/docs/likec4/views-engines.c4 b/docs/likec4/views-engines.c4 index 6b51fd0af..c812d156e 100644 --- a/docs/likec4/views-engines.c4 +++ b/docs/likec4/views-engines.c4 @@ -46,7 +46,7 @@ views { description 'Server-side Http10ServerConnectionStage decodes request bytes and encodes response bytes. Connection closes after each response.' include turbohttp.serverStages.Http10ServerEngine include turbohttp.serverStages.Http10ServerConnectionStage - include turbohttp.serverStages.HttpContextBidiStage + include turbohttp.serverStages.ApplicationBridgeStage include network } @@ -55,7 +55,7 @@ views { description 'Server-side Http11ServerConnectionStage handles chunked TE, Host header validation, keep-alive evaluation, and connection reuse.' include turbohttp.serverStages.Http11ServerEngine include turbohttp.serverStages.Http11ServerConnectionStage - include turbohttp.serverStages.HttpContextBidiStage + include turbohttp.serverStages.ApplicationBridgeStage include network } @@ -64,7 +64,7 @@ views { description 'Server-side Http20ServerConnectionStage handles HPACK decompression, frame decoding, stream multiplexing, flow control, SETTINGS/PING/GOAWAY, and response frame encoding.' include turbohttp.serverStages.Http20ServerEngine include turbohttp.serverStages.Http20ServerConnectionStage - include turbohttp.serverStages.HttpContextBidiStage + include turbohttp.serverStages.ApplicationBridgeStage include network } @@ -73,7 +73,7 @@ views { description 'Server-side Http30ServerConnectionStage handles QPACK decompression, frame decoding over QUIC streams, flow control, GOAWAY, and response encoding.' include turbohttp.serverStages.Http30ServerEngine include turbohttp.serverStages.Http30ServerConnectionStage - include turbohttp.serverStages.HttpContextBidiStage + include turbohttp.serverStages.ApplicationBridgeStage include network } } diff --git a/docs/scenarios.md b/docs/scenarios.md index 23e9a0574..33efd3a0b 100644 --- a/docs/scenarios.md +++ b/docs/scenarios.md @@ -4,33 +4,30 @@ TurboHTTP combines a full HTTP stack with Akka Streams, giving you streaming, ba --- -## Entity Gateway +## Standard ASP.NET Core with Actor Backends -Building a REST API usually means writing controllers, wiring up routes, and manually dispatching to your domain layer. With `MapEntity`, you define message factories for each HTTP verb — TurboHTTP handles actor resolution, Ask/Tell, timeouts, and response mapping for you. +TurboHTTP is a transport layer — your application code uses standard ASP.NET Core routing and DI. Combine it with Akka.NET actors for stateful backends by injecting `ActorSystem` or typed actor references into your handlers: ```csharp -app.MapEntity("/api/orders/{id}", entity => +app.MapGet("/orders/{id}", async (int id, ActorSystem system) => { - entity.UseActorRef(registry => registry.Get()); - entity.WithTimeout(TimeSpan.FromSeconds(5)); - - entity.OnGet((OrderId id) => new GetOrder(id)) - .Ask(ask => ask - .Handle(async (ctx, order) => - { - await ctx.Response.WriteAsJsonAsync(order); - })); - - entity.OnPost((OrderId id, CreateOrderRequest body) => new CreateOrder(id, body)) - .Tell(tell => tell.Produces(HttpStatusCode.Created)); + var orderActor = system.ActorSelection($"/user/orders/order-{id}"); + var order = await orderActor.Ask( + new GetOrder(id), TimeSpan.FromSeconds(5)); + return Results.Ok(order); +}); - entity.OnDelete((OrderId id) => new DeleteOrder(id)) - .Tell(); +app.MapPost("/orders", async (CreateOrderRequest req, ActorSystem system) => +{ + var manager = system.ActorSelection("/user/orders"); + var result = await manager.Ask( + new CreateOrder(req.Items), TimeSpan.FromSeconds(5)); + return Results.Created($"/orders/{result.Id}", result); }); ``` ::: tip Key Insight -You only define what message to create for each HTTP verb — the entity builder does the rest. `Ask` sends the message and waits for a typed response, `Tell` fires and forgets with a configurable status code (202 by default). Timeouts, error handling, and actor resolution are built in — a slow or crashed actor returns a proper HTTP error without blocking other requests. +TurboHTTP reuses an existing `ActorSystem` from DI if one is registered (e.g. via Akka.Hosting). Your server connections and your domain actors share the same system — no extra infrastructure. ::: --- @@ -40,7 +37,7 @@ You only define what message to create for each HTTP verb — the entity builder Server-Sent Events let you push data to clients over a long-lived HTTP connection. TurboHTTP makes this trivial — return an Akka Streams `Source` wrapped in `TurboStreamResults.EventStream`, and the framework handles SSE framing, connection lifecycle, and backpressure for you. ```csharp -app.MapGet("/events/orders", (TurboHttpContext ctx, IOrderEventSource orderEvents) => +app.MapGet("/events/orders", (HttpContext ctx, IOrderEventSource orderEvents) => { var events = orderEvents .AsSource() @@ -64,7 +61,7 @@ The `Source` is materialized when the client connects and torn down when they di When you need to stream binary data — file downloads, video, sensor feeds — you want bytes to flow from the source to the network without piling up in memory. `TurboStreamResults.Stream` takes an Akka Streams `Source` of byte chunks and pipes it directly into the HTTP response body. ```csharp -app.MapGet("/files/{fileId}", (TurboHttpContext ctx, IFileStore fileStore, string fileId) => +app.MapGet("/files/{fileId}", (HttpContext ctx, IFileStore fileStore, string fileId) => { var metadata = fileStore.GetMetadata(fileId); @@ -117,7 +114,7 @@ Over HTTP/2, all 100 requests multiplex on a single connection. Responses arrive TurboHTTP doesn't just use Akka Streams for internal plumbing — it exposes the full operator toolkit for you to shape, merge, and throttle data before it hits the wire. Every operator in the pipeline participates in backpressure, from the data source all the way to the client's TCP receive window. ```csharp -app.MapGet("/metrics/live", (TurboHttpContext ctx, IMetricsSource metrics) => +app.MapGet("/metrics/live", (HttpContext ctx, IMetricsSource metrics) => { var cpuMetrics = metrics.CpuEvents(); var memoryMetrics = metrics.MemoryEvents(); diff --git a/docs/server/aspnet-core.md b/docs/server/aspnet-core.md new file mode 100644 index 000000000..c636a4c7b --- /dev/null +++ b/docs/server/aspnet-core.md @@ -0,0 +1,164 @@ +# Using with ASP.NET Core + +TurboHTTP replaces Kestrel as the transport layer. Everything above the transport — middleware, routing, dependency injection, authentication — is standard ASP.NET Core. This page confirms which patterns work and highlights what's different. + +## The Key Idea + +| Layer | What handles it | +|-------|----------------| +| **Your Application Code** | Middleware, routing, controllers, minimal APIs | +| **ASP.NET Core Hosting** | `IHost`, `IHttpApplication`, `HostingApplication` | +| **TurboHTTP Server** | `ApplicationBridgeStage`, protocol engines (H1/H2/H3), actor hierarchy, TCP/QUIC transport | + +TurboHTTP sits below the `IHttpApplication<TContext>` boundary. When a request arrives, TurboHTTP decodes it into an `IFeatureCollection` and hands it to ASP.NET Core's `HostingApplication`, which runs your middleware pipeline. + +## Middleware + +Standard ASP.NET Core middleware works without changes: + +```csharp +var app = builder.Build(); + +app.UseExceptionHandler("/error"); +app.UseHsts(); +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.Use(async (context, next) => +{ + context.Response.Headers.Append("X-Powered-By", "TurboHTTP"); + await next(context); +}); +``` + +## Routing + +All ASP.NET Core routing patterns work: + +```csharp +// Minimal APIs +app.MapGet("/", () => "Hello"); +app.MapPost("/users", (CreateUserRequest req) => Results.Created($"/users/{req.Id}", req)); + +// Route groups +var api = app.MapGroup("/api/v1"); +api.MapGet("/status", () => Results.Ok("healthy")); + +// Controllers +app.MapControllers(); +``` + +## Dependency Injection + +Standard service registration and injection: + +```csharp +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +app.MapGet("/users/{id}", async (int id, IUserRepository repo) => +{ + var user = await repo.GetByIdAsync(id); + return user is not null ? Results.Ok(user) : Results.NotFound(); +}); +``` + +## Configuration & Options + +Standard `IOptions` patterns, environment variables, and `appsettings.json` all work: + +```csharp +builder.Services.Configure(builder.Configuration.GetSection("MyApp")); + +app.MapGet("/config", (IOptions opts) => Results.Ok(opts.Value)); +``` + +## Authentication & Authorization + +Standard ASP.NET Core auth: + +```csharp +builder.Services.AddAuthentication().AddJwtBearer(); +builder.Services.AddAuthorization(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/secure", () => "Protected").RequireAuthorization(); +``` + +## Health Checks + +```csharp +builder.Services.AddHealthChecks() + .AddCheck("db", () => HealthCheckResult.Healthy()); + +app.MapHealthChecks("/health"); +``` + +## What's Different from Kestrel + +Most things are identical. These are the differences: + +### Connection Model + +Kestrel uses thread-pool-based connection handling. TurboHTTP creates an Akka.NET actor per connection. This means: + +- Each connection has isolated state — no shared mutable state between connections +- Supervision strategies handle connection failures automatically +- Backpressure flows from `ApplicationBridgeStage` through the protocol engine to the transport + +### ActorSystem + +TurboHTTP creates or reuses an `ActorSystem`: + +- If your DI container already has an `ActorSystem` registered (via Akka.Hosting), TurboHTTP reuses it +- If not, TurboHTTP creates its own `ActorSystem` named `turbo-server` + +```csharp +// Share an ActorSystem with Akka.Hosting +builder.Services.AddAkka("my-system", configurationBuilder => +{ + // your Akka.NET config +}); + +builder.Host.UseTurboHttp(options => +{ + options.ListenLocalhost(5000); +}); +// TurboHTTP reuses "my-system" +``` + +### Handler Timeout + +TurboHTTP enforces a per-request handler timeout (default 30s). If your handler doesn't complete within `HandlerTimeout + HandlerGracePeriod`, the request gets a 503 response: + +```csharp +builder.Host.UseTurboHttp(options => +{ + options.HandlerTimeout = TimeSpan.FromSeconds(60); + options.HandlerGracePeriod = TimeSpan.FromSeconds(10); +}); +``` + +### Graceful Shutdown + +TurboHTTP uses Akka Coordinated Shutdown instead of `IHostApplicationLifetime`. See [Hosting & Lifecycle](./hosting) for details. + +## Features That Work Out of the Box + +These ASP.NET Core features require no special configuration with TurboHTTP: + +- Minimal APIs and controllers +- Model binding and validation +- `IResult` return types +- Exception handling middleware +- Static files +- CORS +- Response compression +- Response caching +- Request logging / `W3CLogging` +- OpenTelemetry / diagnostics diff --git a/docs/server/binding.md b/docs/server/binding.md deleted file mode 100644 index 62f8167dd..000000000 --- a/docs/server/binding.md +++ /dev/null @@ -1,332 +0,0 @@ -# Parameter Binding - -TurboHTTP Server automatically binds handler delegate parameters from multiple sources. The binding system inspects the handler's parameters and resolves them from the request context, enabling clean handler signatures without boilerplate request inspection. - -## How It Works - -When you define a handler, the server examines each parameter and determines where to resolve it based on the parameter name, type, and attributes. This happens automatically—no explicit binding configuration needed. - -```csharp -app.MapPost("/users/{id}", async (int id, CreateUserRequest body, CancellationToken ct) => -{ - // id comes from the route template {id} - // body comes from the request JSON body - // ct comes from the cancellation infrastructure -}); -``` - -## Binding Sources - -Parameters are resolved in this order of precedence: - -### 1. Special Types (Highest Priority) - -These are injected directly from the request context, regardless of parameter name: - -- **`TurboHttpContext`** — The complete HTTP context containing request, response, and connection details -- **`CancellationToken`** — Scoped to this handler invocation - -```csharp -app.MapGet("/info", (TurboHttpContext ctx, CancellationToken ct) => -{ - var method = ctx.Request.Method; - var path = ctx.Request.Path; - // Handler will be cancelled if client disconnects -}); -``` - -### 2. Route Values - -Parameters matching route template placeholders are bound by name: - -```csharp -app.MapGet("/posts/{id}/comments/{commentId}", (int id, int commentId) => -{ - // id comes from {id} in the route - // commentId comes from {commentId} in the route -}); -``` - -**Supported route value types:** -- Integers: `int`, `long` -- Decimals: `float`, `double`, `decimal` -- Booleans: `bool` -- Identifiers: `Guid` -- Dates: `DateTime`, `DateTimeOffset` -- Time: `TimeSpan` -- Text: `string` - -::: warning Parsing Behavior -Route values are parsed using `TypeDescriptor.GetConverter()`. If parsing fails, the route does not match. -::: - -### 3. From Header - -Use the `[FromHeader]` attribute to bind from request headers: - -```csharp -app.MapGet("/secure", ([FromHeader] string authorization, [FromHeader("X-API-Key")] string apiKey) => -{ - // authorization comes from the Authorization header - // apiKey comes from the X-API-Key header - // Header names are case-insensitive -}); -``` - -Header names default to the parameter name (with hyphens replacing underscores). Override with the attribute argument. - -::: tip Header Name Mapping -`[FromHeader] string user_agent` binds to the `User-Agent` header automatically. -::: - -### 4. From Query String - -Use the `[FromQuery]` attribute or rely on convention for simple types in GET requests: - -```csharp -app.MapGet("/search", (string q, [FromQuery] int page = 1, [FromQuery("sort-by")] string sortBy = "date") => -{ - // q comes from ?q=... - // page comes from ?page=... - // sortBy comes from ?sort-by=... -}); -``` - -Query parameter names are matched to parameter names (with underscore-to-hyphen conversion). Defaults are respected. - -### 5. From JSON Body - -Complex types in POST, PUT, or PATCH requests are automatically bound from the request body as JSON: - -```csharp -public record CreateUserRequest(string Name, string Email, int Age); - -app.MapPost("/users", async (CreateUserRequest req) => -{ - return new { Message = $"Created user: {req.Name}" }; -}); -``` - -The request body is deserialized into the parameter type. The handler receives the deserialized object. - -```bash -curl -X POST http://localhost:5000/users \ - -H "Content-Type: application/json" \ - -d '{"name":"Alice","email":"alice@example.com","age":30}' -``` - -::: warning Content Type -Body binding only occurs for `Content-Type: application/json`. Other content types are not automatically deserialized. -::: - -### 6. From Body (Explicit) - -Use `[FromBody]` to explicitly bind a parameter to the request body: - -```csharp -app.MapPost("/upload-metadata", ([FromBody] MetadataRequest metadata) => -{ - return new { Message = "Metadata stored" }; -}); -``` - -This is useful for disambiguation when multiple parameters could be interpreted as body parameters. - -### 7. From Form Data - -Use the `[FromForm]` attribute to bind from `application/x-www-form-urlencoded` or `multipart/form-data`: - -```csharp -app.MapPost("/upload", ([FromForm] string title, [FromForm] TurboFormFile file) => -{ - var content = file.Stream; - var fileName = file.FileName; - return new { Message = $"Uploaded: {fileName}" }; -}); -``` - -`TurboFormFile` provides access to uploaded file streams and metadata. - -### 8. Service Injection - -Parameters not matched by any above rule are resolved from the dependency injection container: - -```csharp -// Assume IUserRepository is registered in DI -app.MapGet("/users/{id}", (int id, IUserRepository repo) => -{ - var user = await repo.GetByIdAsync(id); - return user; -}); -``` - -If a parameter type is registered in the DI container and doesn't match earlier binding sources, it's injected. - -::: warning Unresolvable Parameters -If a parameter cannot be bound and is not optional, the route registration fails or throws at invocation time. -::: - -### 9. Context and Cancellation (Recap) - -These are always available and bound first: - -```csharp -app.MapGet("/info", (TurboHttpContext ctx, CancellationToken ct, int? id = null) => -{ - var remoteIP = ctx.Connection.RemoteAddress; - return new { remoteIP }; -}); -``` - -## Binding Order Summary - -This table shows the complete precedence (top to bottom): - -| Order | Source | Binding Method | Example | -|-------|--------|----------------|---------| -| 1 | Special Types | Direct injection | `TurboHttpContext ctx` | -| 1 | Special Types | Direct injection | `CancellationToken ct` | -| 2 | Route Values | Template match + parse | `(int id)` → `{id:int}` | -| 3 | Headers | `[FromHeader]` or named match | `[FromHeader] string authorization` | -| 4 | Query String | `[FromQuery]` or inferred | `[FromQuery] int page` | -| 5 | JSON Body | Auto for complex types in POST/PUT/PATCH | `(CreateUserRequest req)` | -| 6 | Body (Explicit) | `[FromBody]` | `[FromBody] string raw` | -| 7 | Form Data | `[FromForm]` | `[FromForm] string title` | -| 8 | Service Injection | DI container lookup | `(IUserService service)` | - -## Advanced: Composite Parameters with `[AsParameters]` - -Use `[AsParameters]` to bind multiple source values into a single composite type: - -```csharp -public record PaginationFilter( - [FromQuery] int Page = 1, - [FromQuery] int Limit = 10, - [FromQuery] string? Sort = null -); - -app.MapGet("/posts", ([AsParameters] PaginationFilter filter) => -{ - return new { filter.Page, filter.Limit, filter.Sort }; -}); -``` - -This is equivalent to: - -```csharp -app.MapGet("/posts", ( - [FromQuery] int page = 1, - [FromQuery] int limit = 10, - [FromQuery] string? sort = null) => -{ - return new { page, limit, sort }; -}); -``` - -`[AsParameters]` works with: -- Records with constructor parameters -- Classes with settable properties -- Named tuple types - -Each member is bound according to its own attributes and type. - -## Practical Examples - -### Example 1: REST Resource Endpoint - -```csharp -public record UpdateProductRequest(string Name, decimal Price); - -app.MapPut("/products/{id}", async ( - int id, - UpdateProductRequest req, - IProductService service, - CancellationToken ct) => -{ - var product = await service.UpdateAsync(id, req.Name, req.Price, ct); - return Results.Ok(product); -}); -``` - -- `id` from route `{id}` -- `req` from JSON body -- `service` from DI -- `ct` from cancellation infrastructure - -### Example 2: Complex Query Filtering - -```csharp -public record SearchFilter( - [FromQuery] string Q, - [FromQuery] int Page = 1, - [FromQuery] int Limit = 20, - [FromQuery("date-from")] DateTime? DateFrom = null -); - -app.MapGet("/articles", ( - [AsParameters] SearchFilter filter, - IArticleRepository repo) => -{ - return repo.Search(filter.Q, filter.Page, filter.Limit, filter.DateFrom); -}); -``` - -Query: `?q=turbohttp&page=2&limit=50&date-from=2024-01-01` - -### Example 3: File Upload with Metadata - -```csharp -app.MapPost("/files", async ( - [FromForm] string title, - [FromForm] string? description, - [FromForm] TurboFormFile file, - IFileService fileService, - CancellationToken ct) => -{ - using var stream = file.Stream; - var id = await fileService.StoreAsync(title, description, stream, ct); - return Results.Created($"/files/{id}", new { id }); -}); -``` - -### Example 4: Conditional Headers and Request Context - -```csharp -app.MapGet("/data/{id}", ( - int id, - [FromHeader("If-None-Match")] string? etag, - TurboHttpContext ctx) => -{ - var data = GetData(id); - if (etag == data.ETag) - { - ctx.Response.StatusCode = 304; // Not Modified - return null; - } - return data; -}); -``` - -## Best Practices - -::: tip Keep Signatures Clean -Use `[AsParameters]` to group related query/header parameters into records. This improves readability and reusability. -::: - -::: tip Validate Early -Complex types from the body should have constructor validation or use a middleware to validate before handler invocation. -::: - -::: tip Use Cancellation Tokens -Always accept `CancellationToken` in async handlers. It signals graceful shutdown and client disconnection. -::: - -::: warning Avoid Ambiguity -If a parameter could match multiple sources, be explicit with attributes (`[FromQuery]`, `[FromBody]`, etc.). -::: - -## What's Not Bound - -- **Static values** — No global constants or configuration binding -- **Ambient context** — Beyond `TurboHttpContext` and `CancellationToken` -- **Implicit collection binding** — `List` from multiple query values requires custom parsing diff --git a/docs/server/configuration.md b/docs/server/configuration.md index ec65e3253..0b8ebde5d 100644 --- a/docs/server/configuration.md +++ b/docs/server/configuration.md @@ -1,404 +1,127 @@ # Configuration -TurboHTTP Server exposes all configuration through `TurboServerOptions` — connection limits, timeouts, buffer thresholds, and protocol-specific settings. Configuration is code-first and applies when you call `AddTurboKestrel()`. - -::: tip About AddTurboKestrel -TurboHTTP Server is a fully standalone HTTP server — it does not use or depend on Kestrel. The `AddTurboKestrel` method name follows ASP.NET Core configuration conventions for familiarity. -::: - -## General Options - -`TurboServerOptions` controls server-wide behavior across all connections and protocols. - -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| MaxConcurrentConnections | int | 0 (unlimited) | Maximum number of connections allowed. 0 = no limit. | -| MaxConcurrentUpgradedConnections | int | 0 (unlimited) | Maximum number of upgraded connections (WebSocket, etc.). 0 = no limit. | -| KeepAliveTimeout | TimeSpan | 120s | How long to keep idle connections alive. | -| RequestHeadersTimeout | TimeSpan | 30s | Maximum time to receive request headers before timeout. | -| GracefulShutdownTimeout | TimeSpan | 30s | Time to gracefully shut down active connections. | -| BodyBufferThreshold | int | 65536 (64 KiB) | Buffer size for request bodies before streaming to application. | -| BodyConsumptionTimeout | TimeSpan | 30s | Maximum time the application has to consume the request body. | -| ResponseBodyChunkSize | int | 16384 (16 KiB) | Size of chunks when sending response bodies over the network. | -| Http1 | Http1ServerOptions | (see below) | HTTP/1.x-specific options. | -| Http2 | Http2ServerOptions | (see below) | HTTP/2-specific options. | -| Http3 | Http3ServerOptions | (see below) | HTTP/3-specific options. | - -## HTTP/1.x Options - -Controls HTTP/1.0 and HTTP/1.1 behavior. Access via `options.Http1`. - -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| MaxRequestLineLength | int | 8192 | Maximum length of request line (method + target + version). | -| MaxRequestTargetLength | int | 8192 | Maximum length of request target (URI). Limits attack surface for malformed targets. | -| MaxPipelinedRequests | int | 16 | Maximum number of requests allowed in a pipeline (HTTP/1.1 pipelining). | -| MaxChunkExtensionLength | int | 4096 | Maximum length of chunk extensions in chunked transfer encoding. | -| BodyReadTimeout | TimeSpan | 30s | Time limit for reading request body data. | - -**Example: Increase request line limits for APIs with very long URLs** +All server configuration flows through `TurboServerOptions`, passed to `UseTurboHttp()`. ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { - options.Http1.MaxRequestLineLength = 16384; // 16 KiB instead of 8 KiB - options.Http1.MaxRequestTargetLength = 16384; + // configure here }); ``` -## HTTP/2 Options - -Controls HTTP/2 (RFC 9113) behavior. Access via `options.Http2`. - -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| MaxConcurrentStreams | int | 100 | Maximum number of concurrent streams per connection. | -| InitialWindowSize | int | 65535 | Initial flow-control window size (bytes) for each stream. | -| MaxFrameSize | int | 16384 | Maximum payload size for HTTP/2 frames. | -| MaxHeaderListSize | int | 8192 | Maximum size of the decompressed header block. | -| MaxRequestBodySize | long | 30 * 1024 * 1024 (30 MiB) | Maximum size of a single request body. | -| MaxResponseBufferSize | long | 1024 * 1024 (1 MiB) | Maximum size of buffered response data before backpressure. | -| KeepAliveTimeout | TimeSpan | 130s | How long to wait on idle HTTP/2 connections (before sending PING). | -| RequestHeadersTimeout | TimeSpan | 30s | Time to receive request headers. | -| MinRequestBodyDataRate | int | 240 | Minimum bytes-per-second data rate for request body (slowloris protection). | -| MinRequestBodyDataRateGracePeriod | TimeSpan | 5s | Grace period before enforcing minimum data rate. | - -**Example: Lower stream limits for more conservative memory usage** - -```csharp -builder.Services.AddTurboKestrel(options => -{ - options.Http2.MaxConcurrentStreams = 50; // Reduce from 100 - options.Http2.MaxResponseBufferSize = 512 * 1024; // 512 KiB instead of 1 MiB -}); -``` - -## HTTP/3 Options - -Controls HTTP/3 (RFC 9114, QUIC) behavior. Access via `options.Http3`. - -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| MaxConcurrentStreams | int | 100 | Maximum number of concurrent streams per connection. | -| MaxHeaderListSize | int | 8192 | Maximum size of the decompressed header block. | -| EnableWebTransport | bool | false | Enable experimental WebTransport support (unidirectional streams). | -| MaxRequestBodySize | long | 30 * 1024 * 1024 (30 MiB) | Maximum size of a single request body. | -| KeepAliveTimeout | TimeSpan | 130s | How long to keep idle QUIC connections alive. | -| RequestHeadersTimeout | TimeSpan | 30s | Time to receive request headers. | -| MinRequestBodyDataRate | int | 240 | Minimum bytes-per-second data rate for request body (slowloris protection). | -| MinRequestBodyDataRateGracePeriod | TimeSpan | 5s | Grace period before enforcing minimum data rate. | - -**Example: Enable WebTransport for bidirectional communication** - -```csharp -builder.Services.AddTurboKestrel(options => -{ - options.Http3.EnableWebTransport = true; -}); -``` - -## Endpoint Configuration - -Configure IP addresses, ports, and HTTPS settings with `TurboListenOptions`. - -### Listen Methods - -Use one of these on `TurboServerOptions`: - -```csharp -// Listen on specific address and port -options.Listen(IPAddress.Loopback, 5100); - -// Listen on localhost (shorthand) -options.ListenLocalhost(5100); - -// Listen on any IPv4 address (shorthand) -options.ListenAnyIP(5100); - -// Listen on specific address with configuration -options.Listen(IPAddress.Any, 5100, listen => -{ - listen.Protocols = HttpProtocols.Http1AndHttp2; - listen.UseHttps("/path/to/cert.pfx", "password"); -}); - -// Listen with shorthand + configuration -options.ListenLocalhost(5101, listen => -{ - listen.Protocols = HttpProtocols.Http2; - listen.UseHttps(); // Auto-discover certificate -}); -``` - -### TurboListenOptions Properties - -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| Address | IPAddress | (constructor param) | IP address to listen on (e.g. `IPAddress.Any`, `IPAddress.Loopback`). | -| Port | ushort | (constructor param) | TCP/UDP port number (e.g. 80, 443, 5100). | -| Protocols | HttpProtocols | Http1AndHttp2 | Which protocols to support on this endpoint. | - -### HTTPS Configuration - -Enable HTTPS with one of the `UseHttps()` overloads: - -```csharp -// Auto-discover certificate from system store -listen.UseHttps(); - -// Use X509Certificate2 directly -var cert = new X509Certificate2("/path/to/cert.pfx", "password"); -listen.UseHttps(cert); - -// Load certificate from file -listen.UseHttps("/path/to/cert.pfx", "password"); - -// Load certificate with additional options -listen.UseHttps(cert, opts => -{ - opts.EnabledSslProtocols = SslProtocols.Tls13; - opts.HandshakeTimeout = TimeSpan.FromSeconds(15); -}); -``` - -Set HTTPS defaults for all endpoints via `ConfigureHttpsDefaults()`: - -```csharp -builder.Services.AddTurboKestrel(options => -{ - // Defaults apply to all endpoints unless overridden - options.ConfigureHttpsDefaults(https => - { - https.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12; - https.HandshakeTimeout = TimeSpan.FromSeconds(10); - }); - - // This endpoint uses the defaults above - options.ListenLocalhost(5101, listen => listen.UseHttps()); -}); -``` +## General Options -## HTTPS Options +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `HandlerTimeout` | `TimeSpan` | 30s | Maximum time for a request handler to complete | +| `HandlerGracePeriod` | `TimeSpan` | 5s | Extra time after handler timeout before force-closing | +| `GracefulShutdownTimeout` | `TimeSpan` | 30s | Time to drain connections during shutdown | +| `BodyBufferThreshold` | `int` | 64 * 1024 | Request body buffer size before streaming | +| `BodyConsumptionTimeout` | `TimeSpan` | 30s | Time for the app to consume the request body | +| `ResponseBodyChunkSize` | `int` | 16 * 1024 | Chunk size for response body writes | + +## Connection Limits + +Access via `options.Limits`. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `MaxConcurrentConnections` | `int` | 0 (unlimited) | Maximum concurrent connections | +| `MaxConcurrentUpgradedConnections` | `int` | 0 (unlimited) | Maximum upgraded connections (WebSocket) | +| `MaxRequestBodySize` | `long` | 30 * 1024 * 1024 | Global max request body size | +| `MaxRequestHeaderCount` | `int` | 100 | Maximum request headers | +| `MaxRequestHeadersTotalSize` | `int` | 32 * 1024 | Maximum total header bytes | +| `KeepAliveTimeout` | `TimeSpan` | 130s | Idle connection timeout | +| `RequestHeadersTimeout` | `TimeSpan` | 30s | Time to receive request headers | +| `MinRequestBodyDataRate` | `double` | 0 | Minimum body bytes/sec (0 = disabled) | +| `MinRequestBodyDataRateGracePeriod` | `TimeSpan` | 5s | Grace period before enforcing body rate | +| `MinResponseDataRate` | `double` | 0 | Minimum response bytes/sec (0 = disabled) | +| `MinResponseDataRateGracePeriod` | `TimeSpan` | 5s | Grace period before enforcing response rate | -Control SSL/TLS behavior with `TurboHttpsOptions`. +## HTTP/1.x Options -| Property | Type | Default | Purpose | -|----------|------|---------|---------| -| ServerCertificate | X509Certificate2? | null | The certificate to use (if set, overrides CertificatePath). | -| CertificatePath | string? | null | Path to certificate file (.pfx, .pem, etc.). | -| CertificatePassword | string? | null | Password for encrypted certificate files. | -| EnabledSslProtocols | SslProtocols | None (system default) | Which TLS versions to allow (e.g. Tls12, Tls13). | -| ClientCertificateValidationCallback | RemoteCertificateValidationCallback? | null | Custom validation for client certificates (mTLS). | -| HandshakeTimeout | TimeSpan | 10s | Time limit for TLS handshake to complete. | +Access via `options.Http1`. -**Example: Require TLS 1.3 with strict client certificate validation** +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `MaxRequestLineLength` | `int` | 8192 | Maximum bytes for the request line | +| `MaxRequestTargetLength` | `int` | 8192 | Maximum bytes for the request target (URL) | +| `MaxPipelinedRequests` | `int` | 16 | Maximum queued pipelined requests | +| `MaxChunkExtensionLength` | `int` | 4096 | Maximum bytes for chunk extensions | +| `BodyReadTimeout` | `TimeSpan` | 30s | Timeout for reading request body | +| `MaxRequestBodySize` | `long` | 30_000_000 | HTTP/1.x-specific body size limit | +| `MaxHeaderListSize` | `int` | 32 * 1024 | Maximum total header bytes | +| `KeepAliveTimeout` | `TimeSpan?` | null (uses global) | Per-protocol keep-alive override | +| `RequestHeadersTimeout` | `TimeSpan?` | null (uses global) | Per-protocol headers timeout override | -```csharp -options.ListenLocalhost(5443, listen => -{ - listen.UseHttps(cert, https => - { - https.EnabledSslProtocols = SslProtocols.Tls13; - https.HandshakeTimeout = TimeSpan.FromSeconds(5); - https.ClientCertificateValidationCallback = (chain, cert, policy, errors) => - { - // Custom validation logic - return errors == System.Net.Security.SslPolicyErrors.None; - }; - }); -}); -``` +## HTTP/2 Options -## Protocol Selection +Access via `options.Http2`. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `MaxConcurrentStreams` | `int` | 100 | Maximum concurrent streams per connection | +| `InitialConnectionWindowSize` | `int` | 1 * 1024 * 1024 | Connection-level flow control window | +| `InitialStreamWindowSize` | `int` | 768 * 1024 | Per-stream flow control window | +| `MaxFrameSize` | `int` | 16 * 1024 | Maximum HTTP/2 frame payload size | +| `MaxHeaderListSize` | `int` | 32 * 1024 | Maximum total header bytes | +| `HeaderTableSize` | `int` | 4 * 1024 | HPACK dynamic table size | +| `MaxRequestBodySize` | `long` | 30_000_000 | HTTP/2-specific body size limit | +| `MaxResponseBufferSize` | `long` | 64 * 1024 | Response buffering before backpressure | +| `KeepAliveTimeout` | `TimeSpan` | 130s | Connection idle timeout | +| `RequestHeadersTimeout` | `TimeSpan` | 30s | Time to receive request headers | +| `MinRequestBodyDataRate` | `int` | 240 | Minimum body bytes/sec | +| `MinRequestBodyDataRateGracePeriod` | `TimeSpan` | 5s | Grace period before enforcing rate | -Use `HttpProtocols` flag enum to specify which protocols each endpoint supports. +## HTTP/3 Options -| Flag | Value | Purpose | -|------|-------|---------| -| Http1 | 1 | HTTP/1.0 and HTTP/1.1 only. | -| Http2 | 2 | HTTP/2 only. | -| Http1AndHttp2 | 3 | HTTP/1.1 and HTTP/2 (both over TLS, negotiated via ALPN). | -| Http3 | 4 | HTTP/3 over QUIC only. | -| None | 0 | No protocols (not useful — use for clearing flags). | +Access via `options.Http3`. -Protocols are negotiated at connection time via ALPN (Application Layer Protocol Negotiation). +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `MaxConcurrentStreams` | `int` | 100 | Maximum concurrent streams per connection | +| `MaxHeaderListSize` | `int` | 32 * 1024 | Maximum total header bytes | +| `QpackMaxTableCapacity` | `int` | 0 | QPACK dynamic table capacity (0 = static only) | +| `EnableWebTransport` | `bool` | false | Enable WebTransport support | +| `MaxRequestBodySize` | `long` | 30_000_000 | HTTP/3-specific body size limit | +| `KeepAliveTimeout` | `TimeSpan` | 130s | Connection idle timeout | +| `RequestHeadersTimeout` | `TimeSpan` | 30s | Time to receive request headers | +| `MinRequestBodyDataRate` | `int` | 240 | Minimum body bytes/sec | +| `MinRequestBodyDataRateGracePeriod` | `TimeSpan` | 5s | Grace period before enforcing rate | -**Example: Mixed protocol endpoints** +## Example: Full Configuration ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { - // HTTP/1 only (unencrypted) - options.ListenAnyIP(80, listen => - { - listen.Protocols = HttpProtocols.Http1; - }); - - // HTTP/1 + HTTP/2 (TLS, ALPN selects at handshake) - options.ListenLocalhost(443, listen => + // Endpoints + options.ListenLocalhost(5000); + options.ListenLocalhost(5001, listen => { + listen.UseHttps(); listen.Protocols = HttpProtocols.Http1AndHttp2; - listen.UseHttps(cert); }); - - // HTTP/3 only (QUIC) - options.ListenLocalhost(443, listen => - { - listen.Protocols = HttpProtocols.Http3; - listen.UseHttps(cert); - }); -}); -``` - -## Complete Configuration Example - -Here's a full configuration combining multiple options: - -```csharp -using TurboHTTP.Hosting; -using System.Net; -using System.Security.Authentication; -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddTurboKestrel(options => -{ - // General limits - options.MaxConcurrentConnections = 1000; - options.KeepAliveTimeout = TimeSpan.FromSeconds(120); - options.RequestHeadersTimeout = TimeSpan.FromSeconds(30); + // Timeouts + options.HandlerTimeout = TimeSpan.FromSeconds(60); + options.HandlerGracePeriod = TimeSpan.FromSeconds(10); options.GracefulShutdownTimeout = TimeSpan.FromSeconds(30); - - // Buffer strategy - options.BodyBufferThreshold = 64 * 1024; // 64 KiB - options.ResponseBodyChunkSize = 16 * 1024; // 16 KiB - - // HTTP/1.x tuning - options.Http1.MaxRequestLineLength = 8192; - options.Http1.MaxPipelinedRequests = 16; - - // HTTP/2 tuning - options.Http2.MaxConcurrentStreams = 100; - options.Http2.MaxRequestBodySize = 30 * 1024 * 1024; // 30 MiB - options.Http2.MinRequestBodyDataRate = 240; // bytes/sec (slowloris protection) - - // HTTP/3 tuning - options.Http3.MaxConcurrentStreams = 100; - options.Http3.MaxRequestBodySize = 30 * 1024 * 1024; // 30 MiB - - // HTTPS defaults - options.ConfigureHttpsDefaults(https => - { - https.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12; - https.HandshakeTimeout = TimeSpan.FromSeconds(10); - }); - - // HTTP endpoint (localhost) - options.ListenLocalhost(5100); - - // HTTPS endpoint (HTTP/1 + HTTP/2) - options.ListenLocalhost(5101, listen => - { - listen.Protocols = HttpProtocols.Http1AndHttp2; - listen.UseHttps("/path/to/cert.pfx", "password"); - }); - - // HTTP/3 endpoint (QUIC) - options.ListenLocalhost(5102, listen => - { - listen.Protocols = HttpProtocols.Http3; - listen.UseHttps("/path/to/cert.pfx", "password"); - }); - - // Any IP + HTTP/2 - options.ListenAnyIP(8080, listen => - { - listen.Protocols = HttpProtocols.Http2; - listen.UseHttps(); - }); -}); - -var app = builder.Build(); -await app.RunAsync(); -``` -## Configuration via appsettings.json + // Limits + options.Limits.MaxConcurrentConnections = 1000; + options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; -You can also configure endpoints through `appsettings.json` and bind them to `TurboServerOptions`: + // HTTP/2 + options.Http2.MaxConcurrentStreams = 200; + options.Http2.InitialConnectionWindowSize = 2 * 1024 * 1024; -```json -{ - "TurboKestrel": { - "Limits": { - "MaxConcurrentConnections": 1000, - "KeepAliveTimeout": "00:02:00", - "RequestHeadersTimeout": "00:00:30" - }, - "Endpoints": { - "Http": { - "Url": "http://localhost:5100", - "Protocols": "Http1" - }, - "Https": { - "Url": "https://localhost:5101", - "Protocols": "Http1AndHttp2", - "Certificate": { - "Path": "/path/to/cert.pfx", - "Password": "secret" - } - } - } - } -} -``` - -Then load in `Program.cs`: - -```csharp -builder.Services.AddTurboKestrel(builder.Configuration, options => -{ - // Optional: override with code - options.Http2.MaxConcurrentStreams = 50; + // HTTP/3 + options.Http3.MaxConcurrentStreams = 200; }); ``` -## Performance Tuning - -### Connection Limits - -Start conservative and increase based on load testing: - -- **MaxConcurrentConnections**: Set to 2-4× your expected peak connection count (accounts for slow clients, connection drains). -- **MaxConcurrentUpgradedConnections**: For WebSocket or HTTP/2 servers, typically 10-50% of total connections (they're heavier). - -### Body Buffers - -Tune based on typical request sizes: - -- **BodyBufferThreshold**: Increase for APIs that expect large JSON payloads; decrease for mostly small requests. -- **ResponseBodyChunkSize**: Larger chunks (32 KiB+) for high-bandwidth scenarios; smaller (8 KiB) for many concurrent slow clients. - -### Timeouts - -Balance resource cleanup against slow clients: - -- **KeepAliveTimeout**: Shorter (30-60s) for APIs with many clients; longer (2-5m) for long-lived connections. -- **RequestHeadersTimeout**: Short (5-10s) for untrusted clients; longer (30s+) for slow networks. -- **BodyConsumptionTimeout**: Match your application's processing speed. - -### Slowloris Protection - -HTTP/2 and HTTP/3 include **MinRequestBodyDataRate** (default 240 bytes/sec) to prevent slowloris attacks: - -```csharp -options.Http2.MinRequestBodyDataRate = 240; // bytes/sec -options.Http2.MinRequestBodyDataRateGracePeriod = TimeSpan.FromSeconds(5); -``` - -This ensures slow-sending clients are eventually disconnected, freeing resources. - -## See Also +## Next Steps -- [Installation & Setup](./installation) — NuGet packages and endpoint configuration -- [Hosting & Deployment](./hosting) — health checks, graceful shutdown, containerization -- [Architecture Overview](/architecture/) — protocol engines and data flow +- [Using with ASP.NET Core](./aspnet-core) — how TurboHTTP integrates with ASP.NET Core +- [Performance Tuning](./performance) — when and how to tune these options +- [Hosting & Lifecycle](./hosting) — shutdown behavior diff --git a/docs/server/entity-gateway.md b/docs/server/entity-gateway.md deleted file mode 100644 index aef9a574d..000000000 --- a/docs/server/entity-gateway.md +++ /dev/null @@ -1,533 +0,0 @@ -# Entity Gateway - -The Entity Gateway bridges HTTP requests to Akka actors, enabling actor-based request handling within TurboHTTP. Instead of returning immediate responses, route handlers send messages to actors and map the actor's response back to HTTP. This pattern is ideal for stateful entities, CQRS architectures, and event-driven systems. - -Entity routes are registered using `MapTurboEntity()`, where `TKey` identifies the entity type. Each route automatically extracts an entity key from the URL, resolves the corresponding actor, and dispatches the request message to that actor. - -## When to Use Entity Gateway - -- **Stateful entities** — each entity key maps to a persistent actor maintaining state -- **CQRS** — separate read/write models with command handlers returning events or aggregate state -- **Event sourcing** — entities that accumulate events and answer queries about their history -- **Fire-and-forget workflows** — accepting a command and returning 202 Accepted without waiting for completion -- **Resilient request handling** — actors can retry, timeout, and handle failures gracefully - -## Basic Setup - -Register entity routes using `MapTurboEntity()` on the `WebApplication` instance: - -```csharp -var builder = WebApplication.CreateBuilder(); -builder.Services.AddTurboKestrel(); -var app = builder.Build(); - -app.MapTurboEntity("/orders/{id}", entity => -{ - entity.UseResolver(); - - entity.OnGet((int id) => new GetOrder(id)); - entity.OnPost((int id, CreateOrderRequest req) => new CreateOrder(id, req.Items)); - entity.OnPut((int id, UpdateOrderRequest req) => new UpdateOrder(id, req.Status)); - entity.OnDelete((int id) => new CancelOrder(id)); -}); - -await app.RunAsync(); -``` - -The configuration fluent API allows you to: -- Define HTTP methods and their message factories -- Choose an actor resolver strategy -- Optionally specify response mappers and timeouts - -## Message Factories - -A message factory is a delegate that receives the HTTP request (including route values, query parameters, and body) and returns a message object to send to the actor. The factory can accept: - -- **Route values** — parameters from the URL pattern (e.g., `string id` from `/orders/{id}`) -- **Query parameters** — simple types from the query string -- **Request body** — complex types marked with `[FromBody]` -- **Headers** — values marked with `[FromHeader]` -- **Services** — dependencies from the DI container marked with `[FromServices]` -- **TurboHttpContext** — full request context for low-level access - -### Simple Message Factory - -```csharp -entity.OnGet((int id) => new GetOrder(id)); -``` - -Extract the entity key from the route and construct a message. - -### Message Factory with Body - -```csharp -public record CreateOrderDto(decimal Amount); - -entity.OnPost((int id, CreateOrderDto dto) => new PlaceOrder(id, dto.Amount)); -``` - -The second parameter is automatically bound from the request body as JSON. - -### Message Factory with Multiple Parameters - -```csharp -public record UpdateOrderDto(string Status, string Notes); - -entity.OnPut((int id, UpdateOrderDto dto, [FromHeader] string authorization) => - new UpdateOrder(id, dto.Status, dto.Notes, authorization)); -``` - -### Message Factory with Dependency Injection - -```csharp -entity.OnPost((int id, CreateOrderDto dto, IValidator validator) => -{ - var result = validator.Validate(dto); - if (!result.IsValid) - throw new ValidationException(result.Errors); - return new PlaceOrder(id, dto.Amount); -}); -``` - -Resolve services from the DI container by type. - -::: tip -Message factories are **synchronous**. If you need to perform async validation or enrichment, do it in the actor before responding, or use a separate middleware. -::: - -## Entity Actors - -Entity actors receive messages from the Entity Gateway and send responses back. The actor defines the message types and response logic: - -```csharp -public sealed class OrderActor : ReceiveActor -{ - private OrderState _state = new(Guid.NewGuid().ToString(), null, 0m); - - public OrderActor() - { - Receive(Handle); - Receive(Handle); - Receive(Handle); - } - - private void Handle(GetOrder msg) - { - var response = _state.Status == null - ? new NotFoundResponse() - : new OrderResponse(_state.Id, _state.Status, _state.Amount); - - Sender.Tell(response); - } - - private void Handle(PlaceOrder msg) - { - _state = _state with { Status = "pending", Amount = msg.Amount }; - Sender.Tell(new OrderResponse(_state.Id, _state.Status, _state.Amount)); - } - - private void Handle(UpdateOrder msg) - { - if (_state.Status == null) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - _state = _state with { Status = msg.Status }; - Sender.Tell(new OrderResponse(_state.Id, _state.Status, _state.Amount)); - } - - private sealed record OrderState(string Id, string? Status, decimal Amount); -} - -// Message types -public sealed record GetOrder(string Id); -public sealed record PlaceOrder(string Id, decimal Amount); -public sealed record UpdateOrder(string Id, string Status); -public sealed record OrderResponse(string Id, string? Status, decimal Amount); -public sealed record NotFoundResponse(); -``` - -The actor handles each message type and responds using `Sender.Tell()`. The response is matched against registered response mappers and written to the HTTP response. - -## Resolvers - -A resolver locates the actor for a given entity key. TurboHTTP includes two built-in strategies, and you can implement custom resolvers. - -### ChildPerEntityResolver - -Creates a child actor per entity key on demand. The first request for an entity key creates a new actor; subsequent requests reuse the same actor: - -```csharp -entity.UseResolver(); -``` - -The resolver expects a parent actor (usually created during startup) that acts as a factory. This is useful for short-lived or dynamically created entities. - -::: warning -Requires proper actor lifecycle management. Ensure child actors are terminated when no longer needed to avoid memory leaks. -::: - -### RegistryResolver - -Looks up a single, pre-registered actor from Akka.Hosting's `ActorRegistry`. Use this when entities are registered at startup: - -```csharp -public sealed class RegistryResolver : IEntityActorResolver -{ - public ValueTask ResolveAsync( - string entityKey, IServiceProvider services, CancellationToken ct) - { - var registry = services.GetRequiredService(); - return ValueTask.FromResult(registry.Get()); - } -} - -// Usage -entity.UseResolver>(); -``` - -Setup: - -```csharp -builder.Services.AddAkka("actor-system", cfg => -{ - cfg.StartActors((system, registry) => - { - var orderActorRef = system.ActorOf(Props.Create(), "order-actor"); - registry.Register(orderActorRef); - }); -}); -``` - -### Custom Resolver - -Implement `IEntityActorResolver` to define your own resolution strategy: - -```csharp -public sealed class PooledResolver : IEntityActorResolver -{ - public async ValueTask ResolveAsync( - string entityKey, IServiceProvider services, CancellationToken ct) - { - var pool = services.GetRequiredService>(); - return await pool.GetOrCreateAsync(entityKey, ct); - } -} - -// Usage -entity.UseResolver>(); -``` - -The resolver is instantiated at request time. Return the actor reference corresponding to the entity key. - -## Ask vs Tell - -Entity Gateway supports two dispatch patterns: **Ask** (default) and **Tell** (fire-and-forget). - -### Ask Pattern (Default) - -The handler sends a message to the actor and waits for a response: - -```csharp -entity.OnGet((int id) => new GetOrder(id)); -// Returns 200 and the actor's response mapped to JSON -``` - -- Sends the message using the Ask pattern -- Waits for the actor to respond (respects timeout) -- Maps the response using registered mappers -- Returns the HTTP response with the mapped data - -**Status codes:** -- 200 — response received and mapped successfully -- 400 — request parameter binding failed -- 404 — response mapper not found for actor response type -- 504 — Ask timeout (actor didn't respond within timeout) -- 500 — other errors - -### Tell Pattern (Fire-and-Forget) - -Call `AcceptedResponse()` to use fire-and-forget semantics: - -```csharp -entity.OnPost((int id, CreateOrderDto dto) => new PlaceOrder(id, dto.Amount)) - .AcceptedResponse(); -// Returns 202 Accepted immediately, actor processes asynchronously -``` - -- Sends the message using Tell (fire-and-forget) -- Returns 202 Accepted immediately -- Does not wait for or map a response -- No timeout (the actor processes independently) - -**Status codes:** -- 202 — message accepted, will be processed asynchronously -- 400 — request parameter binding failed -- 503 — resolver or dispatch failed - -::: tip -Use Tell for long-running operations where the client doesn't need the result, or for event logging where the actor simply persists data. -::: - -## Response Mapping - -Register mappers to convert actor responses to HTTP responses. Each mapper handles a specific response type: - -```csharp -entity.MapResponse((ctx, resp) => - ctx.Response.WriteAsJsonAsync(resp)); - -entity.MapResponse((ctx, _) => -{ - ctx.Response.StatusCode = 404; - return Task.CompletedTask; -}); - -entity.MapResponse((ctx, err) => -{ - ctx.Response.StatusCode = 400; - return ctx.Response.WriteAsJsonAsync(new { errors = err.Errors }); -}); -``` - -When an actor responds, the gateway finds the mapper matching the response type and invokes it. The mapper is responsible for: -- Setting the HTTP status code -- Writing response headers -- Writing the response body - -### Exact Type Matching - -If you register a mapper for `OrderResponse`, it matches responses of type `OrderResponse` exactly: - -```csharp -entity.MapResponse((ctx, resp) => ...); - -Sender.Tell(new OrderResponse(...)); // Matches -Sender.Tell(new SuccessfulOrderResponse(...)); // Doesn't match (different type) -``` - -### Subtype Matching - -If a more specific mapper doesn't match, the gateway falls back to base type mappers: - -```csharp -public record OrderResponse(string Id); -public record SuccessfulOrderResponse(string Id, string Status) : OrderResponse(Id); - -entity.MapResponse((ctx, resp) => - ctx.Response.WriteAsJsonAsync(resp)); - -Sender.Tell(new SuccessfulOrderResponse("1", "complete")); // Matches OrderResponse mapper -``` - -### Response Mapper Not Found - -If no mapper matches the actor's response type, the gateway returns 500 Internal Server Error. This prevents accidentally exposing internal actor types as HTTP responses. - -Always register mappers for all possible actor response types. - -::: warning -If you forget to register a mapper for a response type, requests will fail with 500. Add mappers for all responses your actors can send. -::: - -## Timeouts - -Timeouts apply to the Ask pattern, protecting against hanging requests. Set default timeouts on the builder or override per-method: - -### Global Timeout - -```csharp -entity.WithTimeout(TimeSpan.FromSeconds(10)); -``` - -Applies to all methods unless overridden. Default is 5 seconds. - -### Per-Method Timeout - -```csharp -entity.OnGet((int id) => new GetOrder(id)) - .WithTimeout(TimeSpan.FromSeconds(30)); - -entity.OnPost((int id, CreateOrderDto dto) => new PlaceOrder(id, dto.Amount)) - .WithTimeout(TimeSpan.FromSeconds(5)); -``` - -Override the global timeout for specific methods. Useful for separating fast reads (high timeout) from slow writes (low timeout). - -**Timeout behavior:** -- If the actor responds within the timeout, the response is mapped normally -- If the timeout expires, the gateway returns 504 Gateway Timeout -- Tell patterns ignore timeouts (no waiting) - -::: tip -Set generous timeouts for queries (10-30s) and tight timeouts for commands (2-5s). This distinguishes between expected slowness and hung actors. -::: - -## Complete Example - -Full working example with an Order entity, actor, messages, resolver, and registration: - -```csharp -using Akka.Actor; -using Akka.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Hosting; -using TurboHTTP.Routing; - -// Message types -public sealed record CreateOrderRequest(decimal Amount); -public sealed record GetOrder(string Id); -public sealed record PlaceOrder(string Id, decimal Amount); -public sealed record CancelOrder(string Id); -public sealed record OrderResponse(string Id, string? Status, decimal Amount); -public sealed record NotFoundResponse(); - -// Entity identifier (used as TKey) -public sealed class OrderId; - -// Actor implementation -public sealed class OrderActor : ReceiveActor -{ - private Dictionary _orders = new(); - - public OrderActor() - { - Receive(Handle); - Receive(Handle); - Receive(Handle); - } - - private void Handle(GetOrder msg) - { - var exists = _orders.TryGetValue(msg.Id, out var order); - if (!exists) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - var (status, amount) = order; - Sender.Tell(new OrderResponse(msg.Id, status, amount)); - } - - private void Handle(PlaceOrder msg) - { - _orders[msg.Id] = ("pending", msg.Amount); - Sender.Tell(new OrderResponse(msg.Id, "pending", msg.Amount)); - } - - private void Handle(CancelOrder msg) - { - if (!_orders.ContainsKey(msg.Id)) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - _orders.Remove(msg.Id); - Sender.Tell(new OrderResponse(msg.Id, "cancelled", 0m)); - } -} - -// Startup -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddTurboKestrel(); - -// Add Akka.Hosting with registry -builder.Services.AddAkka("order-system", cfg => -{ - cfg.StartActors((system, registry) => - { - var parentRef = system.ActorOf(Props.Create(), "orders"); - registry.Register(parentRef); - }); -}); - -var app = builder.Build(); - -// Register entity route -app.MapTurboEntity("/orders/{id}", entity => -{ - entity.UseActorRef(); - - entity.OnGet((int id) => new GetOrder(id)); - entity.OnPost((int id, CreateOrderRequest req) => new PlaceOrder(id, req.Amount)); - entity.OnDelete((int id) => new CancelOrder(id)); -}); - -await app.RunAsync(); -``` - -**Usage:** - -```bash -# Create an order -curl -X POST http://localhost:5000/orders/order-1 \ - -H "Content-Type: application/json" \ - -d '{"amount": 99.99}' - -# Retrieve the order -curl http://localhost:5000/orders/order-1 - -# Cancel the order -curl -X DELETE http://localhost:5000/orders/order-1 - -# 404 for unknown order -curl http://localhost:5000/orders/unknown -``` - -## Error Handling - -### Binding Errors - -If parameter binding fails (invalid route value, missing body, etc.), the gateway returns 400 Bad Request with error details: - -```json -{ - "errors": [ - { - "parameterName": "amount", - "message": "Value must be a positive decimal" - } - ] -} -``` - -### Timeout Errors - -If an Ask times out, the gateway returns 504 Gateway Timeout. No response mapper is invoked. - -### Unmapped Response Types - -If an actor responds with a type that has no registered mapper, the gateway returns 500 Internal Server Error. This is a programming error — add a mapper for all possible response types. - -### Actor Errors - -If an actor throws an exception (or crashes), the Ask pattern detects this and returns 500 Internal Server Error. Implement error handling in the actor: - -```csharp -private void Handle(PlaceOrder msg) -{ - try - { - _orders[msg.Id] = ("pending", msg.Amount); - Sender.Tell(new OrderResponse(msg.Id, "pending", msg.Amount)); - } - catch (Exception ex) - { - Sender.Tell(new ErrorResponse(ex.Message)); - } -} -``` - -## Next Steps - -- [Getting Started](./index) — minimal setup and basic patterns -- [Routing](./routing) — route patterns, parameter binding, and route groups -- [Middleware](./middleware) — composing request handlers -- [Configuration](./configuration) — server options and performance tuning diff --git a/docs/server/hosting.md b/docs/server/hosting.md index 49ac5cccd..7da367a8d 100644 --- a/docs/server/hosting.md +++ b/docs/server/hosting.md @@ -8,15 +8,16 @@ When your ASP.NET Core application starts with TurboHTTP Server configured, the 1. **ActorSystem**: Creates or reuses an Akka.NET ActorSystem (or reuses one from the DI container if already present) 2. **Materializer**: Creates a Streams materializer for the system -3. **ServerSupervisorActor**: Creates the top-level supervisor responsible for the entire server -4. **ListenerActors**: For each configured endpoint, creates a listener that binds the transport (TCP or QUIC) -5. **Coordinated Shutdown**: Hooks into Akka's shutdown lifecycle to ensure graceful termination +3. **ApplicationBridgeStage**: Creates the bridge flow that connects protocol engines to `IHttpApplication<TContext>` +4. **EndpointResolver**: Resolves all configured endpoints into listener bindings +5. **ServerSupervisorActor**: Spawns the top-level supervisor with one `ListenerActor` per endpoint +6. **Coordinated Shutdown**: Hooks into Akka's shutdown lifecycle to ensure graceful termination ```csharp // In Program.cs var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); // Creates one listener options.ListenLocalhost(5101); // Creates another listener @@ -27,10 +28,9 @@ var app = builder.Build(); await app.RunAsync(); // Blocks until shutdown signal ``` -When `app.RunAsync()` is called, the TurboServerHostedService: +When `app.RunAsync()` is called, TurboServer (IServer): - Initializes the actor system and materializer -- Creates the ServerSupervisorActor -- Creates a ListenerActor for each endpoint +- Resolves all configured endpoints - Registers shutdown hooks with Akka Coordinated Shutdown ## Actor Hierarchy @@ -79,9 +79,7 @@ Each active connection runs in a ConnectionActor. It: - Materializes the complete Akka.Streams graph: - Transport inbound/outbound flow - Protocol engine (HTTP/1.0, 1.1, 2, or 3) - - Request/response handling - - Middleware pipeline - - Routing and handler execution + - ApplicationBridgeStage → IHttpApplication<TContext> → ASP.NET Core pipeline - Holds a kill switch to stop processing cleanly - Reports completion (success, error, or shutdown) back to the supervisor @@ -94,10 +92,10 @@ From the moment a client connects until it closes, here's what happens: 1. **Connection arrives**: ListenerActor receives an incoming connection from the transport 2. **ConnectionActor spawned**: A new actor is created for this connection, watched by the listener 3. **Pipeline materialized**: The full Akka.Streams graph is wired up: - - Protocol engine decodes transport bytes into HTTP requests - - Middleware processes each request - - Router finds and executes the handler - - Response is encoded back to bytes and sent + - Protocol engine decodes transport bytes into IFeatureCollection + - ApplicationBridgeStage creates TContext via IHttpApplication.CreateContext() + - ASP.NET Core middleware pipeline processes the request + - Response features are encoded back to bytes and sent 4. **Request loop**: The connection waits for the next request (keep-alive) or closes 5. **Completion**: When the connection closes (client disconnect, keep-alive timeout, error): - ConnectionActor reports completion reason to supervisor @@ -137,7 +135,7 @@ If a request handler is blocked indefinitely (e.g., waiting on unresponsive I/O) Set the timeout in configuration: ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.GracefulShutdownTimeout = TimeSpan.FromSeconds(60); }); @@ -151,7 +149,7 @@ Key options control server and connection behavior: ### Connection Limits ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { // Limit concurrent connections (0 = unlimited) options.MaxConcurrentConnections = 1000; @@ -167,7 +165,7 @@ builder.Services.AddTurboKestrel(options => ### Timeouts ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { // Time to wait for the next request on keep-alive connections options.KeepAliveTimeout = TimeSpan.FromSeconds(120); @@ -186,7 +184,7 @@ builder.Services.AddTurboKestrel(options => ### Buffer and Chunk Sizes ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { // Buffer size before reading request body into memory // Larger uploads are streamed @@ -200,7 +198,7 @@ builder.Services.AddTurboKestrel(options => ### HTTP Protocol Options ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { // HTTP/1.x settings options.Http1.MaxPipelinedRequests = 16; @@ -227,7 +225,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped(); builder.Services.AddSingleton(); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5100); options.GracefulShutdownTimeout = TimeSpan.FromSeconds(30); @@ -238,7 +236,7 @@ var app = builder.Build(); // Register a hosted service for custom shutdown logic builder.Services.AddHostedService(); -app.MapTurboPost("/orders", async (CreateOrderRequest req, IOrderRepository repo) => +app.MapPost("/orders", async (CreateOrderRequest req, IOrderRepository repo) => { var order = await repo.CreateAsync(req.CustomerId, req.Items); return new { id = order.Id }; @@ -271,27 +269,15 @@ public sealed class GracefulShutdownHandler : IHostedService Your handler's `StopAsync` is called during Coordinated Shutdown, before the ActorSystem shuts down. This gives you an opportunity to flush caches, close connections, or notify external systems. ::: tip Combining with health checks -For zero-downtime deployments, pair graceful shutdown with health check middleware: +For zero-downtime deployments, use ASP.NET Core's built-in health check middleware: ```csharp -var shuttingDown = false; +builder.Services.AddHealthChecks(); -app.UseTurbo(async (context, next) => -{ - if (shuttingDown && context.Request.Path != "/health") - { - context.Response.StatusCode = 503; - await context.Response.WriteAsync("Service shutting down"); - return; - } - await next(context); -}); - -// Health endpoint stays up during graceful shutdown -app.MapTurboGet("/health", () => new { status = "ok" }); +app.MapHealthChecks("/health"); ``` -This way, load balancers detect the server is draining and route new requests elsewhere, while existing connections finish their work. +Load balancers query `/health` to detect when the server is draining and route new requests elsewhere. ::: ## Transport Layer diff --git a/docs/server/index.md b/docs/server/index.md index 6d8325a8a..952847a0e 100644 --- a/docs/server/index.md +++ b/docs/server/index.md @@ -1,310 +1,73 @@ -# Getting Started with TurboHTTP Server +# TurboHTTP Server -TurboHTTP Server is a high-performance, standalone HTTP server for .NET built on Akka.Streams. It provides middleware, routing, entity gateway, parameter binding, and actor-based connection lifecycle management — all with zero buffer copies and minimal allocations. +TurboHTTP Server is a high-performance `IServer` implementation for ASP.NET Core, built on Akka.Streams. It replaces Kestrel as the transport layer — handling TCP/QUIC connections, HTTP protocol negotiation, and wire-format encoding — while your application continues to use standard ASP.NET Core middleware, routing, and dependency injection. -::: tip New to TurboHTTP Server? -See [Installation & Setup](./installation) for NuGet packages and endpoint configuration. +::: tip Drop-In Replacement +TurboHTTP is not a framework. It's a transport. Register it with `UseTurboHttp()`, then write standard ASP.NET Core code. ::: ## Quick Start -Add the NuGet package to your ASP.NET Core project: - -```bash -dotnet add package TurboHTTP -``` - -Configure the server in `Program.cs`: - ```csharp -using TurboHTTP.Hosting; - var builder = WebApplication.CreateBuilder(args); -// Register TurboHTTP Server -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { - // Configure HTTP endpoint options.ListenLocalhost(5100); - - // Configure HTTPS endpoint - options.ListenLocalhost(5101, listen => - { - listen.UseHttps(); - listen.Protocols = HttpProtocols.Http1AndHttp2; - }); }); var app = builder.Build(); -// Add middleware — TurboHTTP-style pipeline -app.UseTurbo(async (context, next) => -{ - context.Response.Headers.Add("X-Powered-By", "TurboHTTP"); - await next(context); -}); - -// Health check -app.MapTurboGet("/health", () => new { status = "healthy" }); - -// Simple route -app.MapTurboGet("/", () => "Hello, TurboHTTP!"); - -// Route group with sub-routes -var api = app.MapTurboGroup("/api/v1"); -api.MapGet("/users", GetUsers); -api.MapPost("/users", CreateUser); -api.MapGet("/users/{id}", GetUser); +app.MapGet("/", () => "Hello from TurboHTTP!"); await app.RunAsync(); - -// Handlers -static object GetUsers() => new { users = new[] { "Alice", "Bob" } }; - -static object GetUser(int id) => new { id, name = "User " + id }; - -static object CreateUser() => new { created = true }; -``` - -Run the server: - -```bash -dotnet run -``` - -Then test with curl: - -```bash -# HTTP -curl http://localhost:5100/ -curl http://localhost:5100/health - -# HTTPS -curl --insecure https://localhost:5101/api/v1/users -curl --insecure https://localhost:5101/api/v1/users/42 -``` - -## Middleware Pipeline - -TurboHTTP middleware follows the ASP.NET Core pattern — compose request processing from reusable components: - -```csharp -// Inline middleware -app.UseTurbo(async (context, next) => -{ - // Run before - context.Response.Headers.Add("X-Request-ID", Guid.NewGuid().ToString()); - await next(context); - // Run after - context.Response.Headers.Add("X-Processing-Time", "42ms"); -}); - -// Typed middleware (implements ITurboMiddleware) -app.UseTurbo(); - -// Terminal middleware (does not call next) -app.RunTurbo(context => -{ - context.Response.StatusCode = 200; - return context.Response.WriteAsync("Terminal handler\n"); -}); - -// Map to prefix -app.MapTurbo("/debug", builder => -{ - builder.UseTurbo(async (context, next) => - { - context.Response.Headers.Add("X-Debug", "true"); - await next(context); - }); - builder.RunTurbo(context => context.Response.WriteAsync("Debug info\n")); -}); - -// Conditional routing -app.MapTurboWhen( - context => context.Request.Path.StartsWithSegments("/admin"), - builder => - { - builder.UseTurbo(); - builder.MapTurboGet("/dashboard", () => "Admin dashboard"); - }); -``` - -## Routing - -Map HTTP methods to handler functions. Handlers can return POCOs (automatically JSON-serialized), strings, or handle the response directly: - -```csharp -// GET with no parameters -app.MapTurboGet("/items", () => new[] { "item1", "item2" }); - -// GET with route parameter -app.MapTurboGet("/items/{id}", (int id) => new { Id = id, Name = $"Item {id}" }); - -// GET with query parameter -app.MapTurboGet("/search", (string query) => new { Query = query, Results = new object[] { } }); - -// POST with body -app.MapTurboPost("/items", (ItemRequest req) => new { Created = true, Item = req }); - -// PUT -app.MapTurboPut("/items/{id}", (int id, ItemRequest req) => new { Updated = true, Id = id }); - -// DELETE -app.MapTurboDelete("/items/{id}", (int id) => new { Deleted = true, Id = id }); - -// PATCH -app.MapTurboPatch("/items/{id}", (int id, PatchRequest req) => new { Patched = true }); - -// Route groups -var api = app.MapTurboGroup("/api"); -api.MapTurboGet("/status", () => "OK"); - -var v1 = api.MapTurboGroup("/v1"); -v1.MapTurboGet("/users", GetAllUsers); -v1.MapTurboPost("/users", CreateNewUser); -``` - -## Entity Gateway - -Route directly to stateful Akka.NET actors for entity management. Each entity (e.g. Order, User, Account) gets its own actor, keeping state in memory with automatic persistence: - -```csharp -app.MapTurboEntity("/orders/{id}", entity => -{ - // Inject the resolver (how to spawn/route to the actor) - entity.UseResolver(); - - // Map HTTP methods to actor messages - entity.OnGet((int id) => new GetOrder(id)); - entity.OnPost((int id, CreateOrderRequest req) => new CreateOrder(id, req.Items)); - entity.OnPut((int id, UpdateOrderRequest req) => new UpdateOrder(id, req.Status)); - entity.OnDelete((int id) => new CancelOrder(id)); -}); -``` - -The `OrderEntityResolver` spawns/locates order actors and handles routing: - -```csharp -public class OrderEntityResolver : IEntityResolver -{ - private readonly ActorSystem _system; - - public OrderEntityResolver(ActorSystem system) - { - _system = system; - } - - public IActorRef ResolveEntity(int orderId) - { - // Spawn or look up the actor for this order - return _system.ActorOf($"order-{orderId}"); - } -} - -public class OrderActor : ReceiveActor -{ - public OrderActor() - { - Receive(msg => - { - Sender.Tell(new OrderResponse { Id = msg.Id, Status = "Confirmed" }); - }); - } -} -``` - -## Configuration - -Configure endpoints, protocols, and certificates: - -```csharp -builder.Services.AddTurboKestrel(options => -{ - // HTTP on localhost - options.ListenLocalhost(5100); - - // HTTPS on specific address - options.ListenLocalhost(5101, listen => - { - listen.UseHttps(); - }); - - // HTTPS with certificate - options.ListenLocalhost(5102, listen => - { - listen.UseHttps("/path/to/cert.pfx", "password"); - }); - - // Any IPv4 address - options.ListenAnyIP(5100); - - // Specific address - options.Listen(IPAddress.Any, 5100); -}); -``` - -Or use `appsettings.json`: - -```json -{ - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:5100" - }, - "Https": { - "Url": "https://localhost:5101", - "Certificate": { - "Path": "/path/to/cert.pfx", - "Password": "secret" - } - } - } - } -} -``` - -::: tip -The configuration section name `Kestrel` follows ASP.NET Core conventions for familiarity. TurboHTTP reads this section but does not use Kestrel — it's a standalone server. -::: - -Then use configuration in `Program.cs`: - -```csharp -builder.Services.AddTurboKestrel(builder.Configuration, options => -{ - // Optional: override with code -}); ``` ## What's Included -TurboHTTP Server works out of the box with minimal configuration. - -| Feature | Description | -|------------------------------|------------------------------------------------------------------------------------------------------------------| -| **Middleware Pipeline** | ASP.NET Core-style middleware composition with `Use`, `Run`, `Map`, and `MapWhen` for flexible request handling | -| **Routing** | Minimal API-style route registration with `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch` | -| **Entity Gateway** | Route HTTP requests directly to stateful Akka.NET actors for per-entity state management | -| **Parameter Binding** | Automatic binding of route parameters, query strings, and request bodies to handler function arguments | -| **Standalone Server** | Actor-based HTTP server with TCP/QUIC transport via Servus.Akka.Transport | -| **Actor Lifecycle** | Supervisor → Listener → Connection actor hierarchy with graceful shutdown and coordinated termination | +| Feature | Description | +|---------|-------------| +| **IServer implementation** | Drop-in replacement for Kestrel — registers as `IServer` in DI | +| **HTTP/1.0, 1.1, 2, 3** | Full protocol support with ALPN negotiation | +| **Actor-based connections** | Each connection runs in its own Akka.NET actor with supervision | +| **Akka.Streams backpressure** | Reactive streams pipeline protects against slow clients | +| **Graceful shutdown** | Coordinated drain with configurable timeout | +| **IFeatureCollection** | Standard ASP.NET Core feature interfaces for request/response | +| **HTTPS & TLS** | Certificate configuration, ALPN, TLS 1.2/1.3 | +| **TCP & QUIC transport** | Via Servus.Akka.Transport | + +## What TurboHTTP Is NOT + +TurboHTTP does not provide its own middleware, routing, or request handling. It handles the transport layer — everything from the TCP/QUIC socket up through HTTP protocol decoding. Your application code uses: + +- **Standard ASP.NET Core middleware** — `app.Use()`, `app.UseExceptionHandler()`, etc. +- **Standard routing** — `app.MapGet()`, `app.MapPost()`, controllers, minimal APIs +- **Standard DI** — `builder.Services.AddScoped()`, constructor injection +- **Standard configuration** — `appsettings.json`, environment variables, user secrets + +## Supported Feature Interfaces + +TurboHTTP implements these ASP.NET Core feature interfaces per request: + +| Interface | Purpose | +|-----------|---------| +| `IHttpRequestFeature` | Request method, path, headers, body | +| `IHttpResponseFeature` | Response status code, headers | +| `IHttpResponseBodyFeature` | Response body writing | +| `IHttpRequestBodyDetectionFeature` | Whether the request has a body | +| `IHttpResponseTrailersFeature` | HTTP trailer headers | +| `IHttpConnectionFeature` | Connection ID, local/remote addresses | +| `ITlsHandshakeFeature` | TLS protocol, cipher suite | +| `IHttpRequestLifetimeFeature` | Request abort token | +| `IHttpRequestIdentifierFeature` | Unique request identifier | +| `IHttpMaxRequestBodySizeFeature` | Request body size limit | +| `IHttpBodyControlFeature` | Allow synchronous I/O control | ## Next Steps -**Setup:** - -- [Installation & Setup](./installation) — NuGet packages, endpoint configuration, DI registration - -**Feature guides:** - -- [Middleware Pipeline](./middleware) — composition, error handling, CORS, logging -- [Routing & Handlers](./routing) — route parameters, query strings, body binding, route groups -- [Entity Gateway](./entity-gateway) — actors, state management, message routing -- [Configuration](./configuration) — endpoints, protocols, certificates, environment variables -- [Hosting & Deployment](./hosting) — deployment targets, containerization, health checks, graceful shutdown - -**Deep dive:** - -- [Architecture Overview](/architecture/) — four-layer design, data flow, protocol engines, actor hierarchy +- [Installation & Setup](./installation) — NuGet packages, endpoint configuration +- [Configuration](./configuration) — options, timeouts, protocols, HTTPS +- [Using with ASP.NET Core](./aspnet-core) — middleware, routing, DI patterns +- [Hosting & Lifecycle](./hosting) — actor hierarchy, graceful shutdown +- [Performance Tuning](./performance) — concurrency, buffers, timeouts +- [Troubleshooting](./troubleshooting) — common issues and solutions diff --git a/docs/server/installation.md b/docs/server/installation.md index dd2547bb6..b4dd6185c 100644 --- a/docs/server/installation.md +++ b/docs/server/installation.md @@ -17,65 +17,75 @@ Or add it to your `.csproj`: ``` -## Minimal Setup +## Register the Server -The fastest way to get started is to register TurboHTTP with dependency injection and map a single route: +TurboHTTP registers as an `IServer` implementation, replacing Kestrel: ```csharp -using TurboHTTP; -using TurboHTTP.Hosting; -using Microsoft.AspNetCore.Http; - var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5000); }); var app = builder.Build(); - -app.MapTurboGet("/hello", () => TypedResults.Ok("Hello from TurboHTTP")); - +app.MapGet("/", () => "Hello from TurboHTTP!"); await app.RunAsync(); ``` -This creates a server listening on `http://localhost:5000` with HTTP/1.1 and HTTP/2 enabled by default. - -::: tip About AddTurboKestrel -TurboHTTP Server is a fully standalone HTTP server — it does not use or depend on Kestrel. The `AddTurboKestrel` method name follows ASP.NET Core configuration conventions for familiarity. Under the hood, TurboHTTP uses its own TCP/QUIC transport layer (Servus.Akka.Transport). -::: +`UseTurboHttp()` removes any existing `IServer` registration (including Kestrel) and registers `TurboServer`. After this call, your application uses standard ASP.NET Core — `app.MapGet`, `app.Use`, controllers, minimal APIs. ::: tip -`ListenLocalhost(5000)` is equivalent to listening on `127.0.0.1:5000`. Use `ListenAnyIP(5000)` to listen on all IPv4 addresses. +`ListenLocalhost(5000)` binds to `127.0.0.1:5000`. Use `ListenAnyIP(5000)` to listen on all IPv4 addresses. ::: +## Side-by-Side with Kestrel + +The only change from a standard Kestrel setup is replacing the server registration: + +```csharp +// Before (Kestrel — default) +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.MapGet("/", () => "Hello"); +await app.RunAsync(); + +// After (TurboHTTP) +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseTurboHttp(options => // <-- this is the only change +{ + options.ListenLocalhost(5000); +}); +var app = builder.Build(); +app.MapGet("/", () => "Hello"); +await app.RunAsync(); +``` + +Everything else — middleware, routing, DI, configuration — stays exactly the same. + ## Endpoint Configuration ### Multiple Endpoints -Listen on multiple addresses and ports: - ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { - options.ListenLocalhost(5000); // HTTP on localhost:5000 - options.ListenAnyIP(8080); // HTTP on all interfaces:8080 + options.ListenLocalhost(5000); + options.ListenAnyIP(8080); options.ListenLocalhost(5001, listen => { - listen.UseHttps(); // HTTPS on localhost:5001 + listen.UseHttps(); }); }); ``` ### Explicit Address Binding -Use `Listen()` to bind to a specific IP address: - ```csharp using System.Net; -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.Listen(IPAddress.Loopback, 5000); options.Listen(IPAddress.Parse("192.168.1.100"), 8080); @@ -84,61 +94,54 @@ builder.Services.AddTurboKestrel(options => ### Protocol Selection -By default, endpoints support both HTTP/1.1 and HTTP/2. To use a specific protocol: +Endpoints default to HTTP/1.1 and HTTP/2. Override per endpoint: ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ListenLocalhost(5000, listen => { - listen.Protocols = HttpProtocols.Http1; // HTTP/1.1 only + listen.Protocols = HttpProtocols.Http1; }); options.ListenLocalhost(5001, listen => { - listen.Protocols = HttpProtocols.Http2; // HTTP/2 only + listen.UseHttps(); + listen.Protocols = HttpProtocols.Http1AndHttp2; }); options.ListenLocalhost(5002, listen => { - listen.Protocols = HttpProtocols.Http1AndHttp2; // Default + listen.UseHttps(); + listen.Protocols = HttpProtocols.Http3; }); }); ``` -Supported protocols: - -| Protocol | Value | Use Case | -|----------|-------|----------| -| `Http1` | HTTP/1.1 only | Legacy clients, maximum compatibility | -| `Http2` | HTTP/2 (ALPN h2) | Modern clients, multiplexing, server push | -| `Http1AndHttp2` | HTTP/1.1 and HTTP/2 (default) | Protocol negotiation via ALPN | -| `Http3` | HTTP/3 (QUIC) | Ultra-low latency, UDP-based | +| Protocol | Value | Transport | Notes | +|----------|-------|-----------|-------| +| `Http1` | HTTP/1.1 only | TCP | Maximum compatibility | +| `Http2` | HTTP/2 only | TCP | Multiplexing, HPACK compression | +| `Http1AndHttp2` | Both (default) | TCP | ALPN negotiation selects protocol | +| `Http3` | HTTP/3 | QUIC (UDP) | Requires HTTPS | ::: warning -HTTP/3 support is **not yet available** in this release. Use `Http1AndHttp2` or `Http1` on HTTPS endpoints. +HTTP/3 requires HTTPS. Configuring `Http3` without `UseHttps()` throws at startup. ::: ## HTTPS Configuration -### Self-Signed or Generated Certificate - -Use TurboHTTP's built-in certificate handling: +### Dev Certificate ```csharp -builder.Services.AddTurboKestrel(options => +options.ListenLocalhost(5001, listen => { - options.ListenLocalhost(5001, listen => - { - listen.UseHttps(); // Auto-generates a certificate - }); + listen.UseHttps(); }); ``` ### Certificate from File -Load a certificate from disk: - ```csharp options.ListenLocalhost(443, listen => { @@ -146,14 +149,19 @@ options.ListenLocalhost(443, listen => }); ``` -### Certificate from X509Certificate2 - -Provide a certificate object directly: +PEM certificates are also supported: ```csharp -using System.Security.Cryptography.X509Certificates; +options.ListenLocalhost(443, listen => +{ + listen.UseHttps("certs/server.pem"); +}); +``` + +### Certificate Object -var cert = new X509Certificate2("certs/server.pfx", "password123"); +```csharp +var cert = X509CertificateLoader.LoadPkcs12FromFile("certs/server.pfx", "password123"); options.ListenLocalhost(443, listen => { @@ -163,55 +171,57 @@ options.ListenLocalhost(443, listen => ### HTTPS Defaults -Configure SSL protocol versions and handshake timeout globally: +Apply settings to all HTTPS endpoints: ```csharp -builder.Services.AddTurboKestrel(options => +builder.Host.UseTurboHttp(options => { options.ConfigureHttpsDefaults(https => { - https.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13; - https.HandshakeTimeout = TimeSpan.FromSeconds(10); + https.EnabledSslProtocols = SslProtocols.Tls13; + https.HandshakeTimeout = TimeSpan.FromSeconds(15); }); - options.ListenLocalhost(443, listen => + options.ListenLocalhost(5001, listen => { - listen.UseHttps(); // Inherits global HTTPS defaults + listen.UseHttps(); // inherits defaults }); }); ``` -**TurboHttpsOptions** properties: - | Property | Type | Default | Description | |----------|------|---------|-------------| | `ServerCertificate` | `X509Certificate2?` | null | Certificate object | -| `CertificatePath` | `string?` | null | Path to .pfx file | +| `CertificatePath` | `string?` | null | Path to .pfx or .pem file | | `CertificatePassword` | `string?` | null | Certificate password | -| `EnabledSslProtocols` | `SslProtocols` | `None` | TLS versions (e.g., Tls12, Tls13) | -| `ClientCertificateValidationCallback` | `RemoteCertificateValidationCallback?` | null | Client cert validation | +| `EnabledSslProtocols` | `SslProtocols` | `None` (OS default) | Allowed TLS versions | | `HandshakeTimeout` | `TimeSpan` | 10 seconds | TLS handshake timeout | +| `ClientCertificateMode` | `ClientCertificateMode` | `NoCertificate` | Client cert requirement | +| `ClientCertificateValidationCallback` | `Callback?` | null | Custom client cert validation | +| `ServerCertificateSelector` | `Func?` | null | SNI-based cert selection | -## Configuration from appsettings.json +Full types for the abbreviated entries: +- `ClientCertificateValidationCallback`: `RemoteCertificateValidationCallback?` +- `ServerCertificateSelector`: `Func?` -Use `IConfiguration` to externalize endpoint and HTTPS settings: +### Connection Logging + +Enable per-connection logging for debugging: ```csharp -builder.Services.AddTurboKestrel( - builder.Configuration, - options => - { - // Optional: override or add endpoints programmatically - options.ListenLocalhost(9000); - } -); +options.ListenLocalhost(5000, listen => +{ + listen.UseConnectionLogging(); +}); ``` -In `appsettings.json`: +## Configuration from appsettings.json + +TurboHTTP reads endpoint configuration from the `TurboHTTP` section: ```json { - "TurboKestrel": { + "TurboHTTP": { "Endpoints": { "Http": { "Url": "http://localhost:5000" @@ -234,59 +244,10 @@ In `appsettings.json`: } ``` -::: tip -Endpoint configuration keys (e.g., `Endpoints:Http`, `Endpoints:Https`) are arbitrary — use meaningful names for your use case. Multiple endpoints are supported by adding more keys under `Endpoints`. -::: - -### Configuration Structure - -**Endpoints:** -- `Url` (required) — full URL (http/https, host, port) -- `Protocols` (optional) — `Http1`, `Http2`, `Http1AndHttp2`, `Http3` -- `Certificate` (optional for HTTPS) — sub-object with `Path` and `Password` -- `SslProtocols` (optional) — comma-separated TLS versions - -**HttpsDefaults:** -- `SslProtocols` (optional) — applies to all HTTPS endpoints without explicit override -- `HandshakeTimeout` (optional) — TimeSpan format (e.g., `00:00:30`) - -## Server Options Reference - -Beyond endpoint configuration, TurboServerOptions exposes performance and protocol-level tuning: - -```csharp -builder.Services.AddTurboKestrel(options => -{ - // Connection limits - options.MaxConcurrentConnections = 1000; - options.MaxConcurrentUpgradedConnections = 500; - - // Timeouts - options.KeepAliveTimeout = TimeSpan.FromSeconds(120); - options.RequestHeadersTimeout = TimeSpan.FromSeconds(30); - options.GracefulShutdownTimeout = TimeSpan.FromSeconds(30); - options.BodyConsumptionTimeout = TimeSpan.FromSeconds(30); - - // Buffering - options.BodyBufferThreshold = 65536; // Request body threshold - options.ResponseBodyChunkSize = 16384; // Response chunk size - - // Protocol-specific settings available via options.Http1, options.Http2, options.Http3 - options.Http2.MaxConcurrentStreams = 100; - options.Http2.MaxFrameSize = 16384; - options.Http2.MaxHeaderListSize = 8192; - - options.Http3.MaxConcurrentStreams = 100; - options.Http3.MaxHeaderListSize = 8192; - options.Http3.EnableWebTransport = false; - - // Endpoints - options.ListenLocalhost(5000); -}); -``` +Endpoint names (`Http`, `Https`) are arbitrary — use meaningful names for your setup. ## Next Steps -- [Getting Started](./index) — routing, middleware, and basic patterns -- [Configuration](./configuration) — all TurboServerOptions explained -- [API Reference](/api/) — full public API surface +- [Configuration](./configuration) — all server options in detail +- [Using with ASP.NET Core](./aspnet-core) — middleware, routing, DI patterns +- [Hosting & Lifecycle](./hosting) — actor hierarchy, graceful shutdown diff --git a/docs/server/middleware.md b/docs/server/middleware.md deleted file mode 100644 index efe33a96a..000000000 --- a/docs/server/middleware.md +++ /dev/null @@ -1,287 +0,0 @@ -# Middleware Pipeline - -TurboHTTP Server implements an ASP.NET Core-style middleware pipeline that allows you to compose request handlers with cross-cutting concerns. Middleware components run in order and can inspect, modify, or short-circuit the request/response flow. - -## How Middleware Works - -The middleware pipeline is built as a delegate chain. Each middleware receives two parameters: -- **context**: The `TurboHttpContext` containing request, response, and connection details -- **next**: A `TurboRequestDelegate` that invokes the next middleware in the pipeline - -Middleware follows a **before/after pattern**: code before `await next(context)` runs on the way in, code after runs on the way out. If you don't call `next()`, the pipeline terminates and no further middleware executes. - -```csharp -public delegate Task TurboRequestDelegate(TurboHttpContext context); - -public interface ITurboMiddleware -{ - Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next); -} -``` - -## Inline Middleware - -For simple, single-use middleware, use `app.UseTurbo()` with an inline delegate: - -```csharp -// Logging middleware -app.UseTurbo(async (context, next) => -{ - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - try - { - await next(context); - } - finally - { - stopwatch.Stop(); - Console.WriteLine($"{context.Request.Method} {context.Request.Path} " + - $"completed in {stopwatch.ElapsedMilliseconds}ms with status {context.Response.StatusCode}"); - } -}); -``` - -```csharp -// CORS headers middleware -app.UseTurbo(async (context, next) => -{ - context.Response.Headers["Access-Control-Allow-Origin"] = "*"; - context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"; - - if (context.Request.Method == "OPTIONS") - { - context.Response.StatusCode = 204; - return; - } - - await next(context); -}); -``` - -```csharp -// Authorization check -app.UseTurbo(async (context, next) => -{ - var token = context.Request.Headers["Authorization"].FirstOrDefault(); - if (string.IsNullOrEmpty(token)) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized"); - return; - } - - await next(context); -}); -``` - -## Class-Based Middleware - -For reusable, complex middleware, implement `ITurboMiddleware`: - -```csharp -public class TimingMiddleware : ITurboMiddleware -{ - private readonly ILogger _logger; - - public TimingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - try - { - await next(context); - } - finally - { - stopwatch.Stop(); - _logger.LogInformation( - "Request {Method} {Path} completed in {ElapsedMilliseconds}ms with status {StatusCode}", - context.Request.Method, - context.Request.Path, - stopwatch.ElapsedMilliseconds, - context.Response.StatusCode); - } - } -} -``` - -Register class-based middleware with generic registration: - -```csharp -app.UseTurbo(); -``` - -::: tip -Class-based middleware supports dependency injection. Constructor parameters are resolved from the request service provider. -::: - -## Terminal Middleware - -Terminal middleware handles all remaining requests and does not call `next()`. Use `app.RunTurbo()` to register a terminal handler: - -```csharp -app.RunTurbo(async context => -{ - if (context.Request.Path == "/health") - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync("OK"); - } - else if (context.Request.Path == "/status") - { - context.Response.StatusCode = 200; - context.Response.Headers["Content-Type"] = "application/json"; - await context.Response.WriteAsync("{\"status\":\"running\"}"); - } - else - { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); - } -}); -``` - -::: warning -Only one terminal middleware can be registered. It should be the last `Use` or `Run` call in your pipeline builder, as no middleware registered after it will ever execute. -::: - -## Path Branching - -Use `app.MapTurbo()` to branch the pipeline based on a path prefix: - -```csharp -app.MapTurbo("/api", builder => -{ - builder.Use(async (context, next) => - { - context.Response.Headers["X-API-Version"] = "1"; - await next(context); - }); - - builder.Run(async context => - { - context.Response.StatusCode = 200; - context.Response.Headers["Content-Type"] = "application/json"; - await context.Response.WriteAsync("{\"message\":\"API response\"}"); - }); -}); - -app.RunTurbo(async context => -{ - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); -}); -``` - -Requests to `/api/users`, `/api/status`, etc. are routed to the `/api` branch. All other requests bypass that branch and continue through the main pipeline. - -## Conditional Branching - -Use `app.MapTurboWhen()` to branch based on request properties: - -```csharp -app.MapTurboWhen( - predicate: context => context.Request.Headers["User-Agent"].Contains("Mobile"), - configure: builder => - { - builder.Use(async (context, next) => - { - context.Response.Headers["X-Device-Type"] = "mobile"; - await next(context); - }); - - builder.Run(async context => - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync("{\"type\":\"mobile\"}"); - }); - } -); - -app.RunTurbo(async context => -{ - context.Response.StatusCode = 200; - await context.Response.WriteAsync("{\"type\":\"desktop\"}"); -}); -``` - -The predicate is evaluated for each request. If it returns `true`, the branch pipeline executes. Otherwise, execution continues with subsequent middleware. - -## Execution Order - -Middleware executes in the order it is registered: - -```csharp -app.UseTurbo(async (context, next) => -{ - // Runs first (incoming) - await next(context); - // Runs last (outgoing) -}); - -app.UseTurbo(); -// Runs second (incoming), second-to-last (outgoing) - -app.MapTurbo("/admin", builder => -{ - builder.Run(async context => - { - // Runs third (only for /admin/* requests) - }); -}); - -app.RunTurbo(async context => -{ - // Runs last (incoming), first (outgoing) -}); -``` - -The pipeline is built once at startup. Adding middleware after calling `RunTurbo()` has no effect. - -## ASP.NET Core Comparison - -| Feature | TurboHTTP | ASP.NET Core | -|---------|-----------|--------------| -| Middleware interface | `ITurboMiddleware` | `IMiddleware` | -| Inline delegate | `app.UseTurbo(async (ctx, next) => ...)` | `app.Use(async (ctx, next) => ...)` | -| Class-based registration | `app.UseTurbo()` | `app.UseMiddleware()` | -| Terminal handler | `app.RunTurbo(handler)` | `app.Run(handler)` | -| Path branching | `app.MapTurbo(prefix, builder => ...)` | `app.Map(prefix, app => ...)` | -| Conditional branching | `app.MapTurboWhen(predicate, builder => ...)` | `app.MapWhen(predicate, app => ...)` | -| Context type | `TurboHttpContext` | `HttpContext` | - -TurboHTTP middleware follows the same compositional patterns as ASP.NET Core but operates with the TurboHTTP request/response model and is integrated with Akka.Streams backpressure. - -## TurboHttpContext - -`TurboHttpContext` extends `HttpContext` and provides access to: - -- **Request** — HTTP request details (method, path, headers, query string) -- **Response** — HTTP response object for writing status, headers, and body -- **Connection** — Connection metadata (local/remote addresses) -- **RequestAborted** — `CancellationToken` signaling request cancellation -- **TraceIdentifier** — Unique identifier for request tracing -- **User** — Principal from authentication middleware -- **Items** — Request-scoped dictionary for passing data between middleware -- **RequestServices** — Service provider for dependency injection -- **TurboRequest** — TurboHTTP-specific request properties and methods -- **TurboResponse** — TurboHTTP-specific response properties and methods -- **Materializer** — Akka.Streams materialization context - -Use `context.Items` to pass data between middleware: - -```csharp -app.UseTurbo(async (context, next) => -{ - context.Items["StartTime"] = DateTime.UtcNow; - await next(context); -}); - -app.UseTurbo(); // Can access context.Items["StartTime"] -``` diff --git a/docs/server/performance.md b/docs/server/performance.md new file mode 100644 index 000000000..7078cb9f8 --- /dev/null +++ b/docs/server/performance.md @@ -0,0 +1,118 @@ +# Performance Tuning + +TurboHTTP's defaults work well for most applications. This page explains when and how to tune server options for specific workloads. + +## Concurrency + +### Connection Limits + +```csharp +builder.Host.UseTurboHttp(options => +{ + options.Limits.MaxConcurrentConnections = 1000; +}); +``` + +Default is 0 (unlimited). Set a limit to protect against connection exhaustion. Each connection creates an actor, so memory scales linearly with connection count. + +### HTTP/2 and HTTP/3 Stream Limits + +```csharp +options.Http2.MaxConcurrentStreams = 200; +options.Http3.MaxConcurrentStreams = 200; +``` + +Default is 100 streams per connection. HTTP/2 and HTTP/3 multiplex many requests over one connection — this controls how many can be active simultaneously. + +Higher values improve throughput for clients sending many parallel requests. Lower values reduce per-connection memory and prevent a single connection from dominating server resources. + +## Buffers + +### Request Body Buffer + +```csharp +options.BodyBufferThreshold = 128 * 1024; // 128 KB +``` + +Default is 64 KB. Request bodies smaller than this threshold are buffered in memory. Larger bodies stream directly to the application. + +- **Increase** for APIs that commonly receive medium-sized payloads (64-256 KB) +- **Decrease** for memory-constrained environments or very large upload workloads + +### Response Chunk Size + +```csharp +options.ResponseBodyChunkSize = 32 * 1024; // 32 KB +``` + +Default is 16 KB. Controls the size of chunks when writing response bodies to the network. + +- **Increase** for large response bodies (file downloads, large JSON) +- **Decrease** for low-latency streaming where you want data sent sooner + +### HTTP/2 Flow Control Windows + +```csharp +options.Http2.InitialConnectionWindowSize = 2 * 1024 * 1024; // 2 MB +options.Http2.InitialStreamWindowSize = 1 * 1024 * 1024; // 1 MB +``` + +Larger windows allow more data in flight before the sender must pause. Increase for high-bandwidth, high-latency connections (CDN, cross-region). + +### HTTP/2 Response Buffer + +```csharp +options.Http2.MaxResponseBufferSize = 128 * 1024; // 128 KB +``` + +Default is 64 KB. Responses are buffered up to this size before backpressure kicks in. Increase for handlers that write large responses in bursts. + +## Timeouts + +### Handler Timeout + +```csharp +options.HandlerTimeout = TimeSpan.FromSeconds(60); +options.HandlerGracePeriod = TimeSpan.FromSeconds(10); +``` + +The handler timeout starts when `IHttpApplication.ProcessRequestAsync()` begins. If the handler doesn't complete within `HandlerTimeout`, the request's `CancellationToken` is cancelled. After an additional `HandlerGracePeriod`, TurboHTTP returns a 503 response. + +- **Short (5-10s)**: API endpoints with fast handlers +- **Medium (30s, default)**: General web applications +- **Long (60-120s)**: File uploads, long-polling, report generation + +### Keep-Alive Timeout + +```csharp +options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(60); +``` + +Default is 130s. How long idle connections stay open. Lower values free connection actors sooner. Higher values reduce reconnection overhead for chatty clients. + +### Graceful Shutdown + +```csharp +options.GracefulShutdownTimeout = TimeSpan.FromSeconds(60); +``` + +Default is 30s. Time to drain active connections during shutdown. Set this longer than your longest expected handler execution. + +## Backpressure + +TurboHTTP uses Akka.Streams reactive backpressure throughout the pipeline. When a slow client can't consume response data fast enough, backpressure propagates: + +1. Response body writer blocks (async wait, not thread block) +2. `ApplicationBridgeStage` stops pulling new requests from the protocol engine +3. Protocol engine stops reading from the transport +4. TCP/QUIC flow control signals the client to slow down + +This prevents memory buildup from buffering responses for slow clients. No configuration needed — it's built into the Akka.Streams pipeline. + +## Benchmarking + +Run the included benchmarks: + +```bash +dotnet run --configuration Release --project src/TurboHTTP.Benchmarks/TurboHTTP.Benchmarks.csproj +``` diff --git a/docs/server/routing.md b/docs/server/routing.md deleted file mode 100644 index 0ee6e4bb6..000000000 --- a/docs/server/routing.md +++ /dev/null @@ -1,439 +0,0 @@ -# Routing - -TurboHTTP Server uses a minimal API-style route registration system. Register routes directly on the `WebApplication` instance using extension methods that follow ASP.NET Core conventions, with fluent builders for metadata and configuration. - -## Basic Routes - -Register routes using `MapTurboGet`, `MapTurboPost`, `MapTurboPut`, `MapTurboDelete`, or `MapTurboPatch`: - -```csharp -var builder = WebApplication.CreateBuilder(); -builder.Services.AddTurboKestrel(); -var app = builder.Build(); - -// Simple handler returning a string -app.MapTurboGet("/hello", () => "Hello from TurboHTTP"); - -// Handler returning typed result -app.MapTurboGet("/status", () => TypedResults.Ok(new { Status = "running" })); - -// Handler accepting TurboHttpContext for low-level access -app.MapTurboPost("/echo", (TurboHttpContext context) => -{ - var body = await context.Request.Body.ReadAsStringAsync(); - return Results.Ok(body); -}); - -// Handler with dependency injection -app.MapTurboPost("/data", async (IDataService service) => -{ - var data = await service.GetDataAsync(); - return TypedResults.Ok(data); -}); - -await app.RunAsync(); -``` - -Route handlers are bound at startup and frozen — the route table is immutable after the app starts. - -::: tip -Any delegate or lambda that accepts supported parameter types will work. See [Parameter Binding](#parameter-binding) for what can be injected. -::: - -## Route Patterns - -Patterns consist of literal segments and parameters: - -### Literal Routes - -```csharp -app.MapTurboGet("/health", () => "OK"); -app.MapTurboGet("/api/status", () => TypedResults.Ok()); -``` - -### Route Parameters - -Parameters are enclosed in curly braces. By default, they capture path segments and are bound as strings: - -```csharp -app.MapTurboGet("/users/{id}", (string id) => TypedResults.Ok($"User: {id}")); - -app.MapTurboGet("/posts/{postId}/comments/{commentId}", - (string postId, string commentId) => - TypedResults.Ok(new { Post = postId, Comment = commentId }) -); -``` - -Parameter names are matched to handler arguments by name (case-insensitive): - -```csharp -app.MapTurboGet("/items/{id}", (int id) => -{ - // Parameter 'id' is automatically parsed as int - return TypedResults.Ok($"Item ID: {id}"); -}); -``` - -### Supported Route Value Types - -| Type | Example | Notes | -|------|---------|-------| -| `string` | `/users/{name}` | Default, no parsing needed | -| `int` | `/posts/{id}` | 32-bit signed integer | -| `long` | `/archives/{id}` | 64-bit signed integer | -| `float` | `/temperature/{value}` | Single-precision floating point | -| `double` | `/distance/{value}` | Double-precision floating point | -| `decimal` | `/price/{amount}` | High-precision decimal | -| `bool` | `/settings/{enabled}` | Parses "true"/"false" | -| `Guid` | `/items/{key}` | UUID format | -| `DateTime` | `/events/{date}` | ISO 8601 format | -| `DateTimeOffset` | `/logs/{timestamp}` | Timezone-aware datetime | -| `TimeSpan` | `/delays/{duration}` | ISO 8601 duration format | - -Parse failures for route parameters return a 400 status code automatically. - -::: warning -Parameter names must match handler argument names exactly (case-insensitive). Misnamed parameters are treated as query string parameters or dependency injection targets instead of route values. -::: - -## Parameter Binding - -Handlers can accept multiple types of parameters. The binder infers the source based on the parameter type and optional attributes: - -### Request Properties - -Access the request object directly: - -```csharp -app.MapTurboPost("/upload", async (TurboHttpContext context, HttpRequest request) => -{ - var contentType = request.ContentType; - var body = request.Body; - return TypedResults.Accepted(); -}); -``` - -**Implicit types:** -- `TurboHttpContext` — the full request context -- `HttpRequest` — the HTTP request -- `CancellationToken` — request cancellation token - -### Route Parameters - -Parameters with names matching route segments are automatically bound: - -```csharp -app.MapTurboGet("/users/{userId}/posts/{postId}", - (int userId, int postId) => - TypedResults.Ok(new { UserId = userId, PostId = postId }) -); -``` - -### Query String Parameters - -By default, non-route parameters of simple types are bound from the query string: - -```csharp -app.MapTurboGet("/search", (string q, int page = 1) => - TypedResults.Ok(new { Query = q, Page = page }) -); - -// GET /search?q=hello&page=2 binds q="hello", page=2 -``` - -### Headers - -Use the `[FromHeader]` attribute to bind request headers: - -```csharp -using Microsoft.AspNetCore.Mvc; - -app.MapTurboPost("/data", ([FromHeader] string authorization) => -{ - var token = authorization; // Value of Authorization header - return TypedResults.Ok(); -}); - -app.MapTurboGet("/info", ([FromHeader(Name = "X-Custom")] string custom) => -{ - // Bind custom header, default to "X-Custom" - return TypedResults.Ok(custom); -}); -``` - -### JSON Body - -Use the `[FromBody]` attribute or the parameter type must be a complex type: - -```csharp -public record CreateUserRequest(string Name, string Email); - -app.MapTurboPost("/users", ([FromBody] CreateUserRequest body) => - TypedResults.Created("/users/1", body) -); -``` - -If no explicit attributes are used and the type is a class or interface (not a simple type or service), it is treated as JSON body. - -### Form Data - -Bind form fields and files with `[FromForm]`: - -```csharp -app.MapTurboPost("/upload", - ([FromForm] string name, [FromForm] IFormFile file) => - { - var fileName = file.FileName; - var size = file.Length; - return TypedResults.Accepted(); - } -); -``` - -### Dependency Injection - -Services registered in the DI container are resolved automatically: - -```csharp -public interface IEmailService -{ - Task SendAsync(string to, string subject, string body); -} - -builder.Services.AddScoped(); - -app.MapTurboPost("/notify", async (IEmailService email, string recipient) => -{ - await email.SendAsync(recipient, "Hello", "Welcome!"); - return TypedResults.NoContent(); -}); -``` - -**Rules:** -- Parameters matching route segments are route values -- Simple types (string, int, bool, etc.) default to query string -- Complex types, interfaces, and classes are resolved from DI -- Use explicit attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromForm]`, `[FromServices]`) to override - -## Route Groups - -Group multiple routes under a common prefix using `MapTurboGroup`: - -```csharp -var api = app.MapTurboGroup("/api"); - -api.MapGet("/users", () => TypedResults.Ok()); -api.MapPost("/users", () => TypedResults.Created("/users/1", null)); -api.MapGet("/users/{id}", (int id) => TypedResults.Ok()); -api.MapPut("/users/{id}", (int id) => TypedResults.NoContent()); -api.MapDelete("/users/{id}", (int id) => TypedResults.NoContent()); -``` - -All routes under the group are prefixed with `/api`. - -### Nested Groups - -Groups can be nested: - -```csharp -var api = app.MapTurboGroup("/api"); -var v1 = api.MapGroup("/v1"); -var users = v1.MapGroup("/users"); - -users.MapGet("", () => TypedResults.Ok()); // GET /api/v1/users -users.MapPost("", () => TypedResults.Created("", null)); // POST /api/v1/users -users.MapGet("/{id}", (int id) => TypedResults.Ok()); // GET /api/v1/users/{id} -``` - -### Group Metadata - -Groups support metadata for documentation and filtering (though metadata is not applied to routes at runtime): - -```csharp -var adminApi = app.MapTurboGroup("/admin") - .WithTags("administration") - .WithMetadata(new AuthorizeAttribute()); - -adminApi.MapGet("/stats", () => TypedResults.Ok()); -``` - -Metadata is stored but not enforced by the routing engine. Use it for API documentation, OpenAPI schemas, or custom processing. - -## Route Handler Builder - -The builder returned by `MapTurboGet()`, `MapTurboPost()`, etc. allows you to add metadata and configure the route: - -```csharp -app.MapTurboGet("/users", () => TypedResults.Ok()) - .WithName("GetUsers") - .WithTags("users", "public") - .WithMetadata(new CustomMetadata()) - .Produces>(200) - .ProducesProblem(500); -``` - -### Builder Methods - -| Method | Purpose | -|--------|---------| -| `WithName(string name)` | Assign a name for documentation and routing references | -| `WithTags(params string[] tags)` | Add tags (e.g., "users", "admin") for grouping | -| `WithMetadata(params object[] metadata)` | Store arbitrary metadata objects | -| `RequireAuthorization()` | Mark route as requiring authorization (informational) | -| `AllowAnonymous()` | Mark route as allowing anonymous access (informational) | -| `Produces(int statusCode = 200)` | Declare response type and status code | -| `ProducesProblem(int statusCode = 500)` | Declare problem response status code | - -Metadata is stored on the route but not enforced by the routing engine. Use it for API documentation, OpenAPI generation, or custom middleware that inspects endpoint metadata. - -```csharp -app.MapTurboPost("/items", async (IItemService service) => - TypedResults.Created("/items/1", new { Id = 1 }) -) - .WithName("CreateItem") - .WithTags("items") - .Produces(201) - .ProducesProblem(400); -``` - -## Multi-Method Routes - -Register a single handler for multiple HTTP methods using `MapTurboMethods`: - -```csharp -app.MapTurboMethods( - "/items", - new[] { HttpMethod.Get, HttpMethod.Post, HttpMethod.Put }, - (TurboHttpContext context) => - { - return context.Request.Method switch - { - "GET" => TypedResults.Ok(), - "POST" => TypedResults.Created("/items/1", null), - "PUT" => TypedResults.NoContent(), - _ => TypedResults.BadRequest() - }; - } -); -``` - -The handler receives the full request context and can branch on `context.Request.Method`. - -::: warning -Only use multi-method routes when the same handler genuinely implements multiple methods. Prefer separate `MapTurboGet`, `MapTurboPost`, etc. for clarity. -::: - -## How Routing Works - -### Route Registration (Startup) - -Routes are registered during application startup as you call `MapTurboGet`, `MapTurboPost`, etc. Each route is added to the `TurboRouteTable`: - -1. Pattern is stored as-is (e.g., `/users/{id}`) -2. Handler is bound — parameters are introspected and matched to route/query/service sources -3. A dispatcher is created that will invoke the bound handler at request time - -### Route Freezing - -After the app starts, the route table is frozen. No new routes can be added, and lookups are optimized. - -### Request Matching - -When a request arrives: - -1. **Method matching** — exact HTTP method match required (GET, POST, etc.) -2. **Path matching** — request path is split into segments and compared to route patterns -3. **Segment count** — pattern segments must equal path segments (both count-based) -4. **Literal matching** — literal segments match exactly (case-insensitive) -5. **Parameter capture** — route parameters are extracted and stored in `RouteValues` -6. **Binding** — handler parameters are bound from route values, query string, headers, DI, or JSON body -7. **Invocation** — handler is called with bound arguments - -If no route matches, a 404 response is sent. - -### Middleware Order - -Middleware runs **before** routing. This means: - -- CORS, logging, authentication middleware execute for all requests -- Routing happens after middleware pipeline -- Route handlers are the last step in request processing - -```csharp -// Logging runs for all requests -app.UseTurbo(async (context, next) => -{ - Console.WriteLine($"Incoming {context.Request.Method} {context.Request.Path}"); - await next(context); -}); - -// Route handlers execute here if a route matches -app.MapTurboGet("/hello", () => "Hi"); - -// Terminal fallback for no match -app.RunTurbo(context => context.Response.StatusCode = 404); -``` - -## Entity Routes - -For actor-based CQRS or event-driven request handling, TurboHTTP provides entity routes that map HTTP requests to actor messages and responses. - -Entity routes are a specialized feature that integrate with Akka.Streams and actor systems. See [Entity Gateway](./entity-gateway.md) for full details on configuring entity routes, request/response mapping, and actor resolution. - -```csharp -app.MapTurboEntity("/items/{id}", entity => -{ - entity.UseActorRef(); - entity.OnGet((string id) => new GetItemRequest(id)); - entity.OnPut((string id, UpdateItemRequest req) => new UpdateItem(id, req)); - entity.OnDelete((string id) => new DeleteItem(id)); -}); -``` - -## Error Handling - -### Validation Errors - -Parse errors (invalid route parameter types) return 400 Bad Request automatically. Validation attribute failures also return 400 with error details. - -### Handler Exceptions - -Unhandled exceptions in handlers result in a 500 Internal Server Error. Use middleware to implement custom exception handling. - -```csharp -app.UseTurbo(async (context, next) => -{ - try - { - await next(context); - } - catch (NotFoundException) - { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not found"); - } - catch (UnauthorizedException) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized"); - } - catch (Exception) - { - context.Response.StatusCode = 500; - await context.Response.WriteAsync("Internal error"); - } -}); - -app.MapTurboGet("/items/{id}", async (int id, IItemService service) => -{ - var item = await service.GetItemAsync(id); // May throw NotFoundException - return TypedResults.Ok(item); -}); -``` - -## Next Steps - -- [Getting Started](./index) — minimal setup and basic patterns -- [Middleware](./middleware) — composing request handlers -- [Entity Gateway](./entity-gateway) — actor-based request handling -- [Configuration](./configuration) — server options and performance tuning diff --git a/docs/server/scenarios.md b/docs/server/scenarios.md deleted file mode 100644 index 27d7784d7..000000000 --- a/docs/server/scenarios.md +++ /dev/null @@ -1,718 +0,0 @@ -# Real-World Server Scenarios - -TurboHTTP Server combines routing, middleware, validation, and entity gateway to handle practical application patterns. This page walks through four common scenarios with complete, runnable examples. - -## Scenario 1: REST API with Validation and Entity Gateway - -Build a REST API with Content-Type validation middleware, health endpoints, and entity-based order management using the actor gateway. - -**Use this when you have**: -- Stateful domain entities (orders, users, accounts) -- Need to validate request structure before routing -- Want actor-based message handling with typed responses - -```csharp -using Akka.Actor; -using Akka.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using System.ComponentModel.DataAnnotations; -using TurboHTTP.Hosting; - -// Domain types -public sealed class OrderId; - -public record CreateOrderRequest( - [Required][StringLength(50)] string CustomerId, - [Range(1, int.MaxValue)] decimal Amount, - [Required] string[] ItemIds); - -public record UpdateOrderRequest( - [Required] string Status); - -// Messages -public sealed record GetOrder(string Id); -public sealed record CreateOrder(string CustomerId, decimal Amount, string[] ItemIds); -public sealed record UpdateOrder(string Id, string Status); -public sealed record CancelOrder(string Id); - -// Responses -public sealed record OrderResponse(string Id, string CustomerId, string Status, decimal Amount); -public sealed record NotFoundResponse(); -public sealed record ValidationFailedResponse(string[] Errors); - -// Order actor — handles all order state -public sealed class OrderActor : ReceiveActor -{ - private readonly Dictionary _orders = new(); - - public OrderActor() - { - Receive(Handle); - Receive(Handle); - Receive(Handle); - Receive(Handle); - } - - private void Handle(GetOrder msg) - { - if (!_orders.TryGetValue(msg.Id, out var order)) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - var (customerId, status, amount) = order; - Sender.Tell(new OrderResponse(msg.Id, customerId, status, amount)); - } - - private void Handle(CreateOrder msg) - { - var orderId = Guid.NewGuid().ToString(); - _orders[orderId] = (msg.CustomerId, "pending", msg.Amount); - Sender.Tell(new OrderResponse(orderId, msg.CustomerId, "pending", msg.Amount)); - } - - private void Handle(UpdateOrder msg) - { - if (!_orders.TryGetValue(msg.Id, out var order)) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - var (customerId, _, amount) = order; - _orders[msg.Id] = (customerId, msg.Status, amount); - Sender.Tell(new OrderResponse(msg.Id, customerId, msg.Status, amount)); - } - - private void Handle(CancelOrder msg) - { - if (!_orders.ContainsKey(msg.Id)) - { - Sender.Tell(new NotFoundResponse()); - return; - } - - _orders.Remove(msg.Id); - Sender.Tell(new OrderResponse(msg.Id, "", "cancelled", 0)); - } -} - -// Validation middleware — ensures Content-Type is JSON for POST/PUT -public sealed class ContentTypeValidationMiddleware : ITurboMiddleware -{ - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - if ((context.Request.Method == "POST" || context.Request.Method == "PUT") && - !context.Request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true) - { - context.Response.StatusCode = 400; - await context.Response.WriteAsJsonAsync(new { error = "Content-Type must be application/json" }); - return; - } - - await next(context); - } -} - -// Startup -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddTurboKestrel(options => -{ - options.ListenLocalhost(5100); -}); - -// Add Akka with OrderActor -builder.Services.AddAkka("order-system", cfg => -{ - cfg.StartActors((system, registry) => - { - var orderRef = system.ActorOf(Props.Create(), "order-manager"); - registry.Register(orderRef); - }); -}); - -var app = builder.Build(); - -// Validation middleware on all routes -app.UseTurbo(); - -// Health endpoint (outside validation path) -app.MapTurboGet("/health", () => new { status = "healthy" }); - -// Entity gateway for orders -app.MapTurboEntity("/orders/{id}", entity => -{ - entity.UseResolver(); - - entity.OnGet((int id) => new GetOrder(id)) - .WithTimeout(TimeSpan.FromSeconds(10)); - - entity.OnPost((int id, CreateOrderRequest req) => - new CreateOrder(req.CustomerId, req.Amount, req.ItemIds)) - .WithTimeout(TimeSpan.FromSeconds(5)); - - entity.OnPut((int id, UpdateOrderRequest req) => - new UpdateOrder(id, req.Status)) - .WithTimeout(TimeSpan.FromSeconds(5)); - - entity.OnDelete((int id) => new CancelOrder(id)) - .WithTimeout(TimeSpan.FromSeconds(5)); - - // Response mappers - entity.MapResponse((ctx, resp) => - { - ctx.Response.StatusCode = 200; - return ctx.Response.WriteAsJsonAsync(resp); - }); - - entity.MapResponse((ctx, _) => - { - ctx.Response.StatusCode = 404; - return ctx.Response.WriteAsJsonAsync(new { error = "Order not found" }); - }); -}); - -await app.RunAsync(); -``` - -**Test with curl**: - -```bash -# Health check (no validation) -curl http://localhost:5100/health - -# Create order (validation applies) -curl -X POST http://localhost:5100/orders/order-1 \ - -H "Content-Type: application/json" \ - -d '{ - "customerId": "cust-123", - "amount": 99.99, - "itemIds": ["item-1", "item-2"] - }' - -# Get order -curl http://localhost:5100/orders/order-1 - -# Update order status -curl -X PUT http://localhost:5100/orders/order-1 \ - -H "Content-Type: application/json" \ - -d '{"status": "shipped"}' - -# Cancel order -curl -X DELETE http://localhost:5100/orders/order-1 - -# 404 on unknown order -curl http://localhost:5100/orders/unknown -``` - -**Key points**: -- Validation middleware enforces Content-Type before routing reaches handlers -- Entity gateway routes HTTP methods to actor messages automatically -- Multiple `MapResponse` handlers map different actor response types to HTTP status codes -- Per-method `WithTimeout()` differentiates fast reads (10s) from slower writes (5s) -- Actors hold all state; handlers are stateless - ---- - -## Scenario 2: Middleware Pipeline — Logging + Auth + CORS - -Compose three middleware layers to measure request time, enforce authentication on restricted paths, and allow cross-origin requests. - -**Use this when you have**: -- Need to measure or log all requests -- Some endpoints require authentication, others are public -- Need CORS support for browser clients - -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics; -using TurboHTTP.Hosting; - -// Timing middleware — measures request duration -public sealed class TimingMiddleware : ITurboMiddleware -{ - private readonly ILogger _logger; - - public TimingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - var stopwatch = Stopwatch.StartNew(); - - try - { - await next(context); - } - finally - { - stopwatch.Stop(); - _logger.LogInformation( - "Request {Method} {Path} completed in {ElapsedMilliseconds}ms with status {StatusCode}", - context.Request.Method, - context.Request.Path, - stopwatch.ElapsedMilliseconds, - context.Response.StatusCode); - } - } -} - -// CORS middleware — adds cross-origin headers -public sealed class CorsMiddleware : ITurboMiddleware -{ - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - context.Response.Headers["Access-Control-Allow-Origin"] = "*"; - context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS"; - context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"; - - // Respond to preflight requests - if (context.Request.Method == "OPTIONS") - { - context.Response.StatusCode = 204; - return; - } - - await next(context); - } -} - -// Authorization middleware — validates bearer token -public sealed class AuthorizationMiddleware : ITurboMiddleware -{ - public async Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next) - { - var token = context.Request.Headers["Authorization"] - .FirstOrDefault()? - .Replace("Bearer ", ""); - - if (string.IsNullOrEmpty(token) || !ValidateToken(token)) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsJsonAsync(new { error = "Unauthorized" }); - return; - } - - await next(context); - } - - private static bool ValidateToken(string token) - { - // Demo: accept any token starting with "valid-" - return token.StartsWith("valid-"); - } -} - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddTurboKestrel(options => options.ListenLocalhost(5100)); - -var app = builder.Build(); - -// Layer 1: Timing (outermost — measures all requests) -app.UseTurbo(); - -// Layer 2: CORS (applies to all routes) -app.UseTurbo(); - -// Public routes (no auth required) -app.MapTurboGet("/", () => new { message = "Public endpoint" }); -app.MapTurboGet("/health", () => new { status = "healthy" }); - -// Layer 3: Protected routes (auth required) -// Execution order: Timing → CORS → Auth → Handler -app.MapTurboWhen( - predicate: context => context.Request.Path.StartsWithSegments("/api"), - configure: builder => - { - builder.UseTurbo(); - - var api = builder.MapTurboGroup("/api"); - api.MapGet("/users", () => new { users = new[] { "Alice", "Bob" } }); - api.MapGet("/users/{id}", (int id) => new { id, name = $"User {id}" }); - api.MapPost("/users", (object req) => new { created = true }); - }); - -await app.RunAsync(); -``` - -**Test with curl**: - -```bash -# Public endpoints (no auth needed) -curl http://localhost:5100/ -curl http://localhost:5100/health - -# Protected endpoint without auth — returns 401 -curl http://localhost:5100/api/users - -# Protected endpoint with valid token — returns 200 -curl -H "Authorization: Bearer valid-mytoken" \ - http://localhost:5100/api/users - -# Preflight CORS request -curl -X OPTIONS http://localhost:5100/api/users \ - -H "Origin: http://example.com" - -# Invalid token — returns 401 -curl -H "Authorization: Bearer invalid-token" \ - http://localhost:5100/api/users -``` - -**Key points**: -- Middleware executes in registration order: Timing (outer) → CORS → Auth → Handler -- Code before `await next()` runs on the way in; code after runs on the way out -- `MapTurboWhen()` protects only `/api/*` routes with auth; public routes bypass it -- CORS middleware runs for all requests, including OPTIONS preflight -- Timing middleware measures total time including nested middleware and handler - ---- - -## Scenario 3: Actor-Based CQRS - -Separate read and write paths using distinct message types and actor handlers. Each path can have different timeouts, response types, and business logic. - -**Use this when you have**: -- Read-heavy workloads where queries are fast but commands are slow -- Need different response types for reads vs. writes -- Want to audit or log commands separately from queries - -```csharp -using Akka.Actor; -using Akka.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Hosting; - -// Domain identifier -public sealed class UserId; - -// CQRS message types — separate queries and commands -public sealed record GetUserQuery(string UserId); -public sealed record CreateUserCommand(string UserId, string Name, string Email); -public sealed record UpdateUserCommand(string UserId, string Name); - -// Response types — different shapes for read vs. write -public sealed record UserReadModel(string UserId, string Name, string Email, DateTime CreatedAt); -public sealed record UserWriteResult(string UserId, string Message); -public sealed record UserNotFound(string UserId); - -// CQRS actor — handles both queries and commands -public sealed class UserActor : ReceiveActor -{ - private readonly Dictionary _users = new(); - - public UserActor() - { - Receive(Handle); - Receive(Handle); - Receive(Handle); - } - - private void Handle(GetUserQuery query) - { - // Query path — fast read, no side effects - if (_users.TryGetValue(query.UserId, out var user)) - { - Sender.Tell(new UserReadModel(query.UserId, user.Name, user.Email, user.CreatedAt)); - } - else - { - Sender.Tell(new UserNotFound(query.UserId)); - } - } - - private void Handle(CreateUserCommand cmd) - { - // Command path — write operation, returns write result - if (_users.ContainsKey(cmd.UserId)) - { - Sender.Tell(new UserNotFound(cmd.UserId)); // Or custom DuplicateUserResponse - return; - } - - _users[cmd.UserId] = (cmd.Name, cmd.Email, DateTime.UtcNow); - Sender.Tell(new UserWriteResult(cmd.UserId, "User created successfully")); - } - - private void Handle(UpdateUserCommand cmd) - { - // Command path — update operation - if (!_users.TryGetValue(cmd.UserId, out var user)) - { - Sender.Tell(new UserNotFound(cmd.UserId)); - return; - } - - _users[cmd.UserId] = (cmd.Name, user.Email, user.CreatedAt); - Sender.Tell(new UserWriteResult(cmd.UserId, "User updated successfully")); - } -} - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddTurboKestrel(options => -{ - options.ListenLocalhost(5100); -}); - -builder.Services.AddAkka("cqrs-system", cfg => -{ - cfg.StartActors((system, registry) => - { - var userRef = system.ActorOf(Props.Create(), "user-aggregate"); - registry.Register(userRef); - }); -}); - -var app = builder.Build(); - -// CQRS entity gateway — same actor, different message paths -app.MapTurboEntity("/users/{id}", entity => -{ - entity.UseActorRef(); - - // Read path (query) - entity.OnGet((int id) => new GetUserQuery(id)); - - // Write path (commands) - entity.OnPost((int id, CreateUserRequest req) => - new CreateUserCommand(id, req.Name, req.Email)); - - entity.OnPut((int id, UpdateUserRequest req) => - new UpdateUserCommand(id, req.Name)); -}); - -await app.RunAsync(); - -// DTOs -public record CreateUserRequest(string Name, string Email); -public record UpdateUserRequest(string Name); -``` - -**Test with curl**: - -```bash -# Create user (command — 5s timeout) -curl -X POST http://localhost:5100/users/user-1 \ - -H "Content-Type: application/json" \ - -d '{"name": "Alice", "email": "alice@example.com"}' - -# Get user (query — 30s timeout, returns UserReadModel) -curl http://localhost:5100/users/user-1 - -# Update user (command — 5s timeout, returns UserWriteResult) -curl -X PUT http://localhost:5100/users/user-1 \ - -H "Content-Type: application/json" \ - -d '{"name": "Alice Smith"}' - -# Get unknown user (returns UserNotFound mapped to 404) -curl http://localhost:5100/users/unknown -``` - -**Key points**: -- Separate message types (`GetUserQuery`, `CreateUserCommand`) define read vs. write paths -- Query path uses generous timeout (30s) for potentially expensive reads -- Command path uses tight timeout (5s) to fail fast if writes hang -- Different response types (`UserReadModel` vs. `UserWriteResult`) are mapped independently -- Same actor receives all messages; message handlers separate business logic per operation - ---- - -## Scenario 4: Multi-Protocol Endpoint - -Listen on multiple ports with different protocol combinations: plaintext HTTP/1.1 on port 5100, and HTTPS HTTP/1.1+HTTP/2 on port 5101. Tune performance per protocol. - -**Use this when you have**: -- Need to support both plaintext and encrypted endpoints -- Want HTTP/2 multiplexing only on secure connections -- Need to tune connection and stream limits separately - -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using System.Net; -using System.Security.Authentication; -using TurboHTTP.Hosting; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddTurboKestrel(options => -{ - // HTTP/1.1 plaintext — suitable for internal APIs or load balancer backends - options.ListenAnyIP(5100, listen => - { - listen.Protocols = HttpProtocols.Http1; - }); - - // HTTPS HTTP/1.1 + HTTP/2 — production endpoint with protocol negotiation - options.ListenLocalhost(5101, listen => - { - listen.Protocols = HttpProtocols.Http1AndHttp2; - listen.UseHttps(); // Auto-discover certificate or use development cert - }); - - // Server-wide limits - options.MaxConcurrentConnections = 1000; - options.KeepAliveTimeout = TimeSpan.FromSeconds(120); - options.RequestHeadersTimeout = TimeSpan.FromSeconds(30); - options.GracefulShutdownTimeout = TimeSpan.FromSeconds(30); - - // HTTP/1.1 tuning - options.Http1.MaxRequestLineLength = 8 * 1024; - options.Http1.MaxPipelinedRequests = 16; - options.Http1.BodyReadTimeout = TimeSpan.FromSeconds(30); - - // HTTP/2 tuning (only applies to HTTPS endpoint) - options.Http2.MaxConcurrentStreams = 100; // Limit parallel streams per connection - options.Http2.InitialWindowSize = 64 * 1024; // Per-stream flow control window (64 KiB) - options.Http2.MaxFrameSize = 16 * 1024; // Max frame payload (16 KiB) - options.Http2.MaxHeaderListSize = 8 * 1024; // Decompressed header block limit (8 KiB) - options.Http2.MaxRequestBodySize = 30 * 1024 * 1024; // Per-request body limit (30 MiB) - options.Http2.MinRequestBodyDataRate = 240; // Slowloris protection: bytes/sec - options.Http2.MinRequestBodyDataRateGracePeriod = TimeSpan.FromSeconds(5); - - // HTTPS defaults (applies to all endpoints with .UseHttps()) - options.ConfigureHttpsDefaults(https => - { - https.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12; - https.HandshakeTimeout = TimeSpan.FromSeconds(10); - }); -}); - -var app = builder.Build(); - -// Routes work on both endpoints -app.MapTurboGet("/", () => new { message = "Hello, TurboHTTP" }); -app.MapTurboGet("/health", () => new { status = "healthy" }); - -// API endpoint -var api = app.MapTurboGroup("/api"); -api.MapGet("/status", () => new { uptime = "100%" }); -api.MapPost("/submit", (object body) => new { received = true }); - -// Stream a large response (tests HTTP/2 frame buffering) -app.MapTurboGet("/stream", async context => -{ - context.Response.ContentType = "application/json"; - context.Response.Headers["Transfer-Encoding"] = "chunked"; - - for (int i = 0; i < 1000; i++) - { - await context.Response.WriteAsync($"{{\"chunk\":{i}}}\n"); - } -}); - -await app.RunAsync(); -``` - -**Test with curl**: - -```bash -# HTTP/1.1 plaintext (port 5100) -curl http://localhost:5100/ -curl http://localhost:5100/health - -# Measure HTTP/1.1 connection overhead (slow, no multiplexing) -curl -w "Time: %{time_total}s\n" http://localhost:5100/api/status - -# HTTPS — HTTP/1.1 (port 5101, no multiplexing) -curl --insecure https://localhost:5101/ - -# HTTPS — request protocol version -curl -I --insecure https://localhost:5101/health - -# Test HTTP/2 multiplexing (requires h2 or nghttp2) -# Install: `brew install nghttp2` or `apt-get install nghttp2-client` -nghttp2 -v https://localhost:5101/ - -# Stream test (shows frame buffering behavior) -curl --insecure https://localhost:5101/stream | head -20 - -# Concurrent requests on HTTP/2 (multiplexed in single connection) -# This is much faster on HTTP/2 than HTTP/1.1 due to multiplexing -for i in {1..10}; do - curl --insecure https://localhost:5101/api/status & -done -wait -``` - -**Key points**: -- Port 5100 (plaintext HTTP/1.1) is suitable for internal APIs or behind a reverse proxy -- Port 5101 (HTTPS HTTP/1.1+HTTP/2) uses ALPN to negotiate protocol at TLS handshake -- HTTP/2 tuning applies only to the HTTPS endpoint (HTTP/1.1 doesn't use these options) -- `MaxConcurrentStreams = 100` prevents excessive parallelism per connection -- `MinRequestBodyDataRate = 240 bytes/sec` protects against slowloris attacks on HTTP/2 -- `GracefulShutdownTimeout` allows inflight requests to complete before server exits - ---- - -## Common Patterns - -### Request Context Isolation - -Use `context.Items` to pass data between middleware and handlers: - -```csharp -app.UseTurbo(async (context, next) => -{ - context.Items["RequestId"] = Guid.NewGuid().ToString(); - context.Items["StartTime"] = DateTime.UtcNow; - await next(context); -}); - -app.MapTurboGet("/trace", context => -{ - var requestId = context.Items["RequestId"]?.ToString() ?? "unknown"; - return context.Response.WriteAsJsonAsync(new { requestId }); -}); -``` - -### Entity Gateway with Fire-and-Forget - -Use `AcceptedResponse()` to return 202 Accepted immediately without waiting for the actor: - -```csharp -entity.OnPost((int id, CreateOrderRequest req) => new PlaceOrder(id, req.Amount)) - .AcceptedResponse(); // Returns 202 immediately, actor processes async -``` - -### Per-Handler Error Mapping - -Map different error types in the same entity route: - -```csharp -entity.MapResponse((ctx, err) => -{ - ctx.Response.StatusCode = 400; - return ctx.Response.WriteAsJsonAsync(new { errors = err.Messages }); -}); - -entity.MapResponse((ctx, _) => -{ - ctx.Response.StatusCode = 404; - return Task.CompletedTask; -}); - -entity.MapResponse((ctx, _) => -{ - ctx.Response.StatusCode = 504; - return ctx.Response.WriteAsJsonAsync(new { error = "Request timeout" }); -}); -``` - ---- - -## See Also - -- [Entity Gateway](./entity-gateway) — detailed actor routing and resolver patterns -- [Middleware Pipeline](./middleware) — composition, ordering, and error handling -- [Configuration](./configuration) — endpoint binding and protocol tuning -- [Validation](./validation) — automatic parameter validation with data annotations diff --git a/docs/server/troubleshooting.md b/docs/server/troubleshooting.md index e8da2ceaf..b8242306d 100644 --- a/docs/server/troubleshooting.md +++ b/docs/server/troubleshooting.md @@ -1,379 +1,177 @@ # Troubleshooting -This guide covers common issues when running TurboHTTP Server and practical debugging techniques to resolve them. +Common issues and solutions when running TurboHTTP Server. -## Server Won't Start +## Server Doesn't Start ### Port Already in Use -If you see an error that the port is already in use, either change the port your server listens on or stop the conflicting process. +``` +System.Net.Sockets.SocketException: Address already in use +``` -Check what's using a port: -```powershell +Another process is using the port. Find it with: + +```bash # Windows -netstat -ano | findstr :8080 +netstat -ano | findstr :5000 # Linux/macOS -lsof -i :8080 +lsof -i :5000 ``` -Change your server configuration: -```csharp -app.ListenTcp( - port: 8081, // Change from 8080 to 8081 - host: "127.0.0.1" -); -``` - -### Missing AddTurboKestrel +Change the port or stop the conflicting process. -The TurboHTTP Server integration must be explicitly registered before building the app: +### Missing HTTPS Certificate -```csharp -var builder = WebApplicationBuilder.CreateBuilder(args); - -// This is required -builder.Services.AddTurboKestrel(); - -var app = builder.Build(); +``` +InvalidOperationException: No server certificate configured for HTTPS endpoint ``` -Without calling `AddTurboKestrel()`, the Listen/ListenLocalhost/ListenAnyIP methods won't be available and the app will fail to build. - -### No Endpoints Configured - -At least one endpoint must be configured when starting the server: +You called `UseHttps()` but didn't provide a certificate. Options: ```csharp -var app = builder.Build(); +// Use a dev certificate +listen.UseHttps(); // requires dotnet dev-certs https --trust -// At least one of these is required: -app.ListenTcp(8080, "127.0.0.1"); -app.ListenTcp(8443, "127.0.0.1", "cert.pfx", "password"); +// Use a file +listen.UseHttps("certs/server.pfx", "password"); -app.Run(); +// Use a certificate object +listen.UseHttps(myCert); ``` -Without any endpoints, the server has no address to bind to and cannot start. - -## HTTPS Errors +### HTTP/3 Without HTTPS -### Certificate Not Found - -Verify the certificate file path is correct and the file exists: - -```csharp -var certPath = Path.Combine(Directory.GetCurrentDirectory(), "certs", "cert.pfx"); -if (!File.Exists(certPath)) -{ - throw new FileNotFoundException($"Certificate not found: {certPath}"); -} - -app.ListenTcp(8443, "127.0.0.1", certPath, "password"); ``` - -Use absolute paths to avoid ambiguity about the working directory. - -### Wrong Certificate Password - -Ensure the password matches the certificate: - -```csharp -// Verify password by attempting to load the certificate -var cert = new X509Certificate2(certPath, "password"); +InvalidOperationException: HTTP/3 requires HTTPS ``` -If you receive a password error, regenerate the certificate or verify the password used when creating it. - -### Certificate Expired - -Check the certificate expiration date: +QUIC requires TLS. Add `UseHttps()` to the endpoint: ```csharp -var cert = new X509Certificate2(certPath, "password"); -Console.WriteLine($"Valid from: {cert.NotBefore}"); -Console.WriteLine($"Valid until: {cert.NotAfter}"); -Console.WriteLine($"Is expired: {DateTime.UtcNow > cert.NotAfter}"); +options.ListenLocalhost(5000, listen => +{ + listen.UseHttps(); + listen.Protocols = HttpProtocols.Http3; +}); ``` -Renew the certificate and update the path in your configuration. +## Connection Issues -## Routes Not Matching +### Requests Time Out with 503 -### Pattern Syntax - -Route patterns use the format `{param:type}` for parameters: +TurboHTTP enforces a handler timeout (default 30s). If your handler takes longer: ```csharp -// Correct -app.MapGet("/users/{id:int}", async (int id, TurboHttpContext ctx) => -{ - await ctx.Response.WriteAsync($"User {id}"); -}); - -// Incorrect - parameter name only won't work -app.MapGet("/users/{id}", async (int id, TurboHttpContext ctx) => +builder.Host.UseTurboHttp(options => { - // This won't match because type is missing + options.HandlerTimeout = TimeSpan.FromSeconds(120); + options.HandlerGracePeriod = TimeSpan.FromSeconds(15); }); ``` -Supported types: `int`, `long`, `float`, `double`, `bool`, `guid`, `string` (default). - -### Group Prefix +### Connections Drop Under Load -Routes within a route group use the full path combining group prefix and route pattern: +Check connection limits: ```csharp -app.MapGroup("/api") - .MapGet("/users", ...); // Full path: /api/users - .MapPost("/users", ...); // Full path: /api/users +options.Limits.MaxConcurrentConnections = 0; // 0 = unlimited (default) ``` -Verify the complete path when testing endpoints. - -### Trailing Slash Sensitivity - -Routes are matched exactly, including trailing slashes: +Check HTTP/2 stream limits if clients use multiplexing: ```csharp -app.MapGet("/users", ...); // Matches /users only - -// This will NOT match: -// /users/ (extra slash) -// /Users (different case) +options.Http2.MaxConcurrentStreams = 200; // default 100 ``` -When linking to endpoints or testing, ensure the path exactly matches the route definition. - -## Middleware Not Executing +### Keep-Alive Connections Close Too Soon -### Registration Order Matters - -Middleware runs in the order it is registered. Register middleware before the route handlers that depend on it: +Increase the keep-alive timeout: ```csharp -var app = builder.Build(); - -// Correct: authentication before route handlers -app.UseAuthentication(); -app.UseRouting(); -app.MapGet("/protected", ...) // Can check User from context - -// Incorrect: authentication after routes won't protect them -app.MapGet("/protected", ...); -app.UseAuthentication(); +options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(300); ``` -::: tip -Use the middleware order to implement cross-cutting concerns like logging, authentication, and error handling consistently. -::: +## Protocol Negotiation -### Missing await next(context) - -Middleware must call the next delegate to continue the pipeline: - -```csharp -app.Use(async (context, next) => -{ - // Do something before - await next(context); // REQUIRED - calls next middleware - // Do something after -}); -``` +### HTTP/2 Not Negotiating -If you don't call `await next(context)`, the pipeline stops and subsequent middleware won't run. +HTTP/2 requires ALPN negotiation over TLS. Ensure: -### Terminal Middleware +1. The endpoint has `UseHttps()` configured +2. `Protocols` includes `Http2` (default is `Http1AndHttp2`) +3. The client supports HTTP/2 and ALPN -The `RunTurbo()` method is terminal and stops the pipeline: +For plaintext HTTP/2 (h2c), the client must send the HTTP/2 connection preface directly. -```csharp -app.MapGet("/hello", ...); -app.RunTurbo(); // After this, no more middleware can run -app.MapGet("/world", ...); // This never executes -``` +### HTTP/3 Not Negotiating -Place terminal middleware last in the configuration. +HTTP/3 uses QUIC (UDP). Ensure: -## Entity Gateway Timeouts +1. The endpoint has `UseHttps()` and `Protocols = HttpProtocols.Http3` +2. The OS supports QUIC (Windows 11+, Linux with libmsquic) +3. The client supports HTTP/3 +4. No firewall blocks UDP on the configured port -### Actor Not Responding +## ActorSystem Configuration -If the entity gateway times out trying to reach an actor, the actor may not be alive or configured correctly. +### Using Your Own ActorSystem -Enable Akka logging to see actor lifecycle events: - -```xml - - - - DEBUG - - - -``` - -Check that: -- The actor system is running -- The actor reference is correct -- Message types match what the actor expects - -### Wrong Resolver - -Ensure the resolver matches your actor setup. Two common patterns: +If you use Akka.Hosting, TurboHTTP reuses your `ActorSystem`: ```csharp -// Child-per-entity: creates one actor per entity ID -services.AddEntityGateway(options => -{ - options.EntityResolver = new ChildPerEntityResolver("handlers"); -}); - -// Registry: requires registering entities in advance -services.AddEntityGateway(options => +builder.Services.AddAkka("my-system", configurationBuilder => { - options.EntityResolver = new RegistryResolver(actorSystem, registry); + // your config }); -``` - -Mixing resolvers or misconfiguring the entity paths causes timeout errors. - -::: warning -The resolver must match how you set up your actors. Verify the actor creation strategy and entity naming. -::: - -### Timeout Too Short - -The default timeout may be too short for slow operations. Increase if needed: -```csharp -var defaultTimeout = TimeSpan.FromSeconds(30); -gateway.SendAsync(entityId, message, defaultTimeout); -``` - -But consider fixing the underlying slow operation rather than just increasing the timeout. - -## Connection Limits - -### MaxConcurrentConnections - -Limit the number of concurrent connections to protect server resources: - -```csharp -app.ListenTcp(8080, "127.0.0.1", options => +builder.Host.UseTurboHttp(options => { - options.MaxConcurrentConnections = 1000; // Limit connections - // Set to 0 for unlimited (not recommended for production) + options.ListenLocalhost(5000); }); ``` -If you consistently hit the connection limit, increase it or investigate whether clients are properly closing connections. - -### HTTP/2 Stream Limits - -Each HTTP/2 connection can have a configurable maximum number of concurrent streams: - -```csharp -var options = new Http2ProtocolOptions -{ - MaxConcurrentStreams = 100 -}; -``` - -If clients are opening many streams on a single connection, increase this value. Too low a limit causes stream reset errors. - -## Shutdown Issues - -### Long-Running Requests - -Graceful shutdown gives in-flight requests a timeout to complete. If requests take longer than the timeout, they're forcefully closed. - -Increase the shutdown timeout: - -```csharp -var host = app.Build(); - -await host.StopAsync(timeout: TimeSpan.FromSeconds(60)); // 60 seconds -``` - -::: tip -Monitor request duration in production. If you frequently exceed the shutdown timeout, consider implementing request queuing or prioritization. -::: +If no `ActorSystem` is registered, TurboHTTP creates one named `turbo-server`. -### Body Not Consumed +### Logging -If response bodies are not fully consumed by clients, the server may hold connections open longer than necessary. - -Enable body consumption timeout: +TurboHTTP routes Akka.NET logs through `ILoggerFactory`. To see connection-level logs: ```csharp -app.ListenTcp(8080, "127.0.0.1", options => +options.ListenLocalhost(5000, listen => { - options.BodyConsumptionTimeout = TimeSpan.FromSeconds(10); + listen.UseConnectionLogging(); }); ``` -This forces connection closure if the client doesn't consume the body within the timeout. - -## Debugging Tips - -### Enable Akka Logging - -Set the Akka log level to DEBUG or INFO to see detailed actor activity: +Set the log level in `appsettings.json`: ```json { - "akka": { - "loggers": ["Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog"], - "loglevel": "DEBUG", - "actor": { - "debug": { - "receive": true, - "lifecycle": true - } + "Logging": { + "LogLevel": { + "TurboHTTP.Server.ConnectionLogging": "Debug" } } } ``` -This helps diagnose actor creation, message delivery, and lifecycle issues. +## Graceful Shutdown -### Use Middleware for Logging +### Shutdown Hangs -Add a logging middleware to inspect requests and responses: +If `StopAsync` doesn't return, a handler may be blocked indefinitely. Reduce the timeout: ```csharp -app.Use(async (context, next) => -{ - var startTime = DateTime.UtcNow; - - await next(context); - - var elapsed = DateTime.UtcNow - startTime; - Console.WriteLine($"{context.Request.Method} {context.Request.Path} - {context.Response.StatusCode} ({elapsed.TotalMilliseconds}ms)"); -}); +options.GracefulShutdownTimeout = TimeSpan.FromSeconds(10); ``` -This provides visibility into the request/response lifecycle without code changes. +After the timeout, connections are forcefully closed. -### Check ConnectionCompletionReason - -When a connection closes abnormally, the completion reason provides details: - -```csharp -// In your actor or middleware -if (connection.CompletionReason is not null) -{ - Console.WriteLine($"Connection closed: {connection.CompletionReason}"); -} -``` +### In-Flight Requests Get 503 -Possible reasons include: -- Client closed normally -- Read/write timeout -- Protocol error -- Graceful shutdown -- Resource limits exceeded +During shutdown, new requests are rejected with 503. This is expected. To minimize impact: -Use this to diagnose whether issues are client-side, server-side, or environmental. +1. Use health checks so load balancers detect the drain +2. Set `GracefulShutdownTimeout` long enough for in-flight requests to complete diff --git a/docs/server/validation.md b/docs/server/validation.md deleted file mode 100644 index 23dd5a100..000000000 --- a/docs/server/validation.md +++ /dev/null @@ -1,148 +0,0 @@ -# Validation - -TurboHTTP Server automatically validates handler parameters using standard `System.ComponentModel.DataAnnotations` attributes. The `ParameterValidator` inspects validation attributes on bound parameters and writes a 400 Bad Request response with structured error details if validation fails. - -## Basic Usage - -Decorate your request types with validation attributes. The server validates all parameters after binding: - -```csharp -public record CreateUserRequest( - [Required] [StringLength(100)] string Name, - [Required] [EmailAddress] string Email, - [Range(18, 150)] int Age); - -public class UserHandler -{ - [Post("/users")] - public async Task CreateUser(CreateUserRequest request) - { - // Request is guaranteed to be valid at this point - return Results.Created($"/users/{request.Id}", request); - } -} -``` - -When validation fails, the server automatically returns a 400 Bad Request with error details: - -```json -{ - "errors": { - "Name": ["The Name field is required."], - "Email": ["The Email field is not a valid e-mail address."], - "Age": ["The field Age must be between 18 and 150."] - } -} -``` - -## Supported Attributes - -| Attribute | Behavior | Example | -|-----------|----------|---------| -| `[Required]` | Field must have a value (non-null, non-empty for strings) | `[Required] string Name` | -| `[StringLength(max)]` | String length must not exceed max | `[StringLength(100)]` | -| `[StringLength(min, max)]` | String length must be between min and max | `[StringLength(3, 50)]` | -| `[Range(min, max)]` | Numeric value must be between min and max | `[Range(0, 100)]` | -| `[RegularExpression(pattern)]` | Value must match the regex pattern | `[RegularExpression(@"^\d{5}$")]` | -| `[EmailAddress]` | Value must be a valid email format | `[EmailAddress]` | -| `[Phone]` | Value must be a valid phone number format | `[Phone]` | -| `[Url]` | Value must be a valid URL | `[Url]` | -| `[MinLength(length)]` | Collection or string must have at least length items/characters | `[MinLength(1)]` | -| `[MaxLength(length)]` | Collection or string must have at most length items/characters | `[MaxLength(50)]` | -| `[Compare(property)]` | Value must equal another property (useful for password confirmation) | `[Compare(nameof(Password))]` | - -## Error Response Format - -When validation fails, the server returns HTTP 400 with a JSON body containing an `errors` object. Each property name maps to an array of error messages: - -```json -{ - "errors": { - "Name": [ - "The Name field is required." - ], - "Email": [ - "The Email field is not a valid e-mail address." - ], - "Age": [ - "The field Age must be between 18 and 150." - ] - } -} -``` - -Multiple validation failures on a single field are included in the same array: - -```json -{ - "errors": { - "Password": [ - "The Password field is required.", - "The field Password must be a string with a minimum length of 8 and maximum length of 100." - ] - } -} -``` - -## Validation on Composite Types - -When using `[AsParameters]` to bind multiple types, validation runs recursively on all nested properties: - -```csharp -public record PaginationParams( - [Range(1, int.MaxValue)] int Page, - [Range(1, 100)] int PageSize); - -public record SearchRequest( - [Required] [StringLength(200)] string Query, - [AsParameters] PaginationParams Pagination); - -public class SearchHandler -{ - [Get("/search")] - public async Task Search(SearchRequest request) - { - // Both SearchRequest and PaginationParams are validated - return Results.Ok(); - } -} -``` - -## Custom Validation - -For complex validation logic that can't be expressed with attributes alone, implement `IValidatableObject` on your request type: - -```csharp -public record UpdateProductRequest( - string Name, - decimal Price, - decimal DiscountedPrice) : IValidatableObject -{ - public IEnumerable Validate(ValidationContext context) - { - if (DiscountedPrice > Price) - { - yield return new ValidationResult( - "Discounted price cannot be greater than regular price.", - new[] { nameof(DiscountedPrice) }); - } - - if (Price <= 0) - { - yield return new ValidationResult( - "Price must be greater than zero.", - new[] { nameof(Price) }); - } - } -} -``` - -The custom validation messages are merged into the error response alongside attribute-based validation errors. - -::: tip -Validation runs automatically after binding completes. There is no need to explicitly call a validation method in your handler — if the handler method executes, validation has already succeeded. -::: - -::: warning -If a parameter fails to bind (e.g., invalid type conversion), binding errors take precedence and validation is skipped. Always ensure your parameter types can be bound before adding validation attributes. -::: diff --git a/docs/superpowers/plans/2026-05-27-iserver-pipeline-redesign.md b/docs/superpowers/plans/2026-05-27-iserver-pipeline-redesign.md new file mode 100644 index 000000000..6a15f28d1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-iserver-pipeline-redesign.md @@ -0,0 +1,1456 @@ +# IServer Pipeline Redesign — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Strip TurboHTTP to a pure transport+protocol layer where `IFeatureCollection` is the stream element, `ApplicationBridgeStage` bridges directly to ASP.NET's `IHttpApplication`, and all custom routing/context types are deleted. + +**Architecture:** The Akka Streams pipeline changes from `Protocol → RequestContext → RoutingStage → TurboHttpContext → Handler` to `Protocol → IFeatureCollection → ApplicationBridgeStage → IFeatureCollection → Response Encoder`. Everything between protocol decoding and ASP.NET's `IHttpApplication` is a single generic stage. No wrappers, no custom routing, no custom context types. + +**Tech Stack:** C# 13, .NET 10, Akka.NET Streams, ASP.NET Core `IServer`/`IHttpApplication`, xUnit v3 + +**Spec:** `docs/superpowers/specs/2026-05-27-iserver-pipeline-redesign.md` + +--- + +## File Map + +### Files to Create +- `src/TurboHTTP/Server/FeatureCollectionFactory.cs` — Pooled factory replacing ServerContextFactory + +### Files to Rewrite +- `src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs` — Full rewrite as `ApplicationBridgeStage` + +### Files to Modify (Production) +| File | Change | +|------|--------| +| `src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs` | Ports: `RequestContext` → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs` | `OnRequest(IFeatureCollection)`, remove `TurboConnectionInfo` | +| `src/TurboHTTP/Protocol/IServerStateMachine.cs` | `OnResponse(IFeatureCollection)` | +| `src/TurboHTTP/Streams/IServerProtocolEngine.cs` | BidiFlow generic args → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs` | Queue/port types → `IFeatureCollection` | +| `src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs` | Self-contained CTS, no RequestContext dep | +| `src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs` | Self-contained TraceIdentifier, no RequestContext dep | +| `src/TurboHTTP/Context/Features/TurboHttpConnectionFeature.cs` | Remove TurboConnectionInfo dep, use fields directly | +| `src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs` | `OnResponse(IFeatureCollection)`, use FeatureCollectionFactory | +| `src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs` | `OnResponse(IFeatureCollection)`, use FeatureCollectionFactory | +| `src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs` | `Encode(Span, IFeatureCollection, ...)` | +| `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs` | `OnResponse(IFeatureCollection)` | +| `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs` | `OnResponse(IFeatureCollection)`, use FeatureCollectionFactory | +| `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs` | `EncodeHeaders(IFeatureCollection, ...)` | +| `src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs` | `IFeatureCollection` instead of `RequestContext` | +| `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs` | `OnResponse(IFeatureCollection)` | +| `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs` | `OnResponse(IFeatureCollection)`, use FeatureCollectionFactory | +| `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs` | `EncodeHeaders(IFeatureCollection)` | +| `src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs` | `OnResponse(IFeatureCollection)` | +| `src/TurboHTTP/Protocol/IProtocolSwitchCapable.cs` | Remove RequestContext using if present | +| `src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs` | Shape port casts → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs` | Shape port casts → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs` | Shape port casts → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs` | Shape port casts → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs` | Shape port casts → `IFeatureCollection` | +| `src/TurboHTTP/Streams/Http10ServerEngine.cs` | BidiFlow type args | +| `src/TurboHTTP/Streams/Http11ServerEngine.cs` | BidiFlow type args | +| `src/TurboHTTP/Streams/Http20ServerEngine.cs` | BidiFlow type args | +| `src/TurboHTTP/Streams/Http30ServerEngine.cs` | BidiFlow type args | +| `src/TurboHTTP/Streams/NegotiatingServerEngine.cs` | BidiFlow type args | +| `src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs` | Remove `TurboRequestDelegate`/`RouteTable`, add bridge stage flow | +| `src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs` | Remove `TurboRequestDelegate`/`RouteTable`, use bridge stage | +| `src/TurboHTTP/Server/TurboServer.cs` | Wire `IHttpApplication` through to bridge stage | +| `src/TurboHTTP/Server/TurboServerServiceCollectionExtensions.cs` | Remove RouteTable DI | + +### Files to Delete +| File | Reason | +|------|--------| +| `src/TurboHTTP/Streams/Stages/Server/RequestContext.cs` | Replaced by `IFeatureCollection` | +| `src/TurboHTTP/Server/TurboHttpContext.cs` | ASP.NET builds own `HttpContext` | +| `src/TurboHTTP/Context/TurboHttpRequest.cs` | No consumer | +| `src/TurboHTTP/Context/TurboHttpResponse.cs` | No consumer | +| `src/TurboHTTP/Server/TurboConnectionInfo.cs` | ASP.NET uses `IHttpConnectionFeature` | +| `src/TurboHTTP/Streams/Stages/Server/RoutingStage.cs` | No custom routing | +| `src/TurboHTTP/Server/RouteTable.cs` | No custom routing | +| `src/TurboHTTP/Server/TurboRequestDelegate.cs` | No custom pipeline | +| `src/TurboHTTP/Server/ServerContextFactory.cs` | Replaced by FeatureCollectionFactory | + +### Test Files to Modify +| File | Change | +|------|--------| +| `src/TurboHTTP.Tests.Shared/FakeServerOps.cs` | `OnRequest(IFeatureCollection)`, `List` | +| `src/TurboHTTP.Tests.Shared/ServerTestContext.cs` | Return `IFeatureCollection` instead of `RequestContext` | +| `src/TurboHTTP.Tests.Shared/ServerTestContextBuilder.cs` | `Build()` returns `IFeatureCollection` | +| `src/TurboHTTP.Tests/Server/ServerContextFactorySpec.cs` | Rename + test FeatureCollectionFactory | +| `src/TurboHTTP.Tests/Server/ContextPoolingSpec.cs` | Test IFeatureCollection pooling | +| All state machine specs (~15 files) | Use `IFeatureCollection` for OnResponse calls | + +--- + +## Task 1: Self-Contained Feature Implementations + +Remove `RequestContext` dependency from the two feature types that delegate to it. After this task these features own their own state. + +**Files:** +- Modify: `src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs` +- Modify: `src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs` + +- [ ] **Step 1: Rewrite TurboHttpRequestLifetimeFeature** + +Replace the RequestContext-delegating implementation with self-contained state: + +```csharp +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Context.Features; + +internal sealed class TurboHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature +{ + public CancellationToken RequestAborted { get; set; } + + public void Abort() => RequestAborted = new CancellationToken(true); +} +``` + +- [ ] **Step 2: Rewrite TurboHttpRequestIdentifierFeature** + +Replace the RequestContext-delegating implementation with self-contained state: + +```csharp +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Context.Features; + +internal sealed class TurboHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature +{ + public string TraceIdentifier + { + get => field ??= Guid.NewGuid().ToString("N"); + set; + } +} +``` + +- [ ] **Step 3: Commit** + +``` +git add src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs +git commit -m "refactor: make lifetime and identifier features self-contained" +``` + +--- + +## Task 2: FeatureCollectionFactory + +Replace `ServerContextFactory` (which returns `RequestContext`) with `FeatureCollectionFactory` (returns `IFeatureCollection`). The factory uses the same thread-static pooling pattern. + +**Files:** +- Create: `src/TurboHTTP/Server/FeatureCollectionFactory.cs` +- Modify: `src/TurboHTTP/Context/Features/TurboHttpConnectionFeature.cs` (check if it depends on TurboConnectionInfo constructor) + +- [ ] **Step 1: Read TurboHttpConnectionFeature to check its constructor** + +Check what TurboHttpConnectionFeature needs — it currently takes a `TurboConnectionInfo`. We need to understand if we change this now or later. + +Run: Grep for `class TurboHttpConnectionFeature` and its constructor. + +- [ ] **Step 2: Create FeatureCollectionFactory** + +Create the new factory at `src/TurboHTTP/Server/FeatureCollectionFactory.cs`: + +```csharp +using Microsoft.AspNetCore.Http.Features; +using TurboHTTP.Context.Features; + +namespace TurboHTTP.Server; + +internal static class FeatureCollectionFactory +{ + [ThreadStatic] + private static Stack? t_pool; + + private const int MaxPoolSize = 32; + + public static IFeatureCollection Create( + TurboHttpRequestFeature requestFeature, + bool hasBody, + IServiceProvider? services = null, + IHttpConnectionFeature? connectionFeature = null, + TlsHandshakeFeature? tlsFeature = null) + { + TurboFeatureCollection features; + + if ((t_pool?.Count ?? 0) > 0) + { + features = t_pool!.Pop(); + } + else + { + features = new TurboFeatureCollection(); + } + + features.Set(requestFeature); + + var bodyFeature = new TurboRequestBodyFeature { Body = requestFeature.Body }; + features.Set(bodyFeature); + + var responseFeature = new TurboHttpResponseFeature(); + features.Set(responseFeature); + + var detectionFeature = new TurboHttpRequestBodyDetectionFeature(hasBody); + features.Set(detectionFeature); + + var responseBodyFeature = new TurboHttpResponseBodyFeature(); + features.Set(responseBodyFeature); + + var trailersFeature = new TurboHttpResponseTrailersFeature(); + features.Set(trailersFeature); + + if (connectionFeature is not null) + { + features.Set(connectionFeature); + } + + if (tlsFeature is not null) + { + features.Set(tlsFeature); + } + + var lifetimeFeature = new TurboHttpRequestLifetimeFeature(); + features.Set(lifetimeFeature); + + var identifierFeature = new TurboHttpRequestIdentifierFeature(); + features.Set(identifierFeature); + + return features; + } + + internal static void Return(IFeatureCollection features) + { + if (features is not TurboFeatureCollection turboFeatures) + { + return; + } + + t_pool ??= new Stack(MaxPoolSize); + + if (t_pool.Count < MaxPoolSize) + { + t_pool.Push(turboFeatures); + } + } +} +``` + +Note: The old `ServerContextFactory` took `TurboConnectionInfo?` and created `TurboHttpConnectionFeature` internally. The new factory takes `IHttpConnectionFeature?` directly — the connection feature is created by `HttpConnectionServerStageLogic` from transport info (Task 5 will update this). + +- [ ] **Step 3: Commit** + +``` +git add src/TurboHTTP/Server/FeatureCollectionFactory.cs +git commit -m "feat: add FeatureCollectionFactory returning IFeatureCollection" +``` + +--- + +## Task 3: Core Interface Changes + +Change all four core interfaces/types to use `IFeatureCollection` instead of `RequestContext`. The codebase will NOT compile after this task until Tasks 4-6 are complete. + +**Files:** +- Modify: `src/TurboHTTP/Protocol/IServerStateMachine.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs` +- Modify: `src/TurboHTTP/Streams/IServerProtocolEngine.cs` + +- [ ] **Step 1: Update IServerStateMachine** + +```csharp +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Transport; + +namespace TurboHTTP.Protocol; + +internal interface IServerStateMachine +{ + bool CanAcceptResponse { get; } + bool ShouldComplete { get; } + int MaxQueuedRequests { get; } + + void PreStart(); + void OnResponse(IFeatureCollection features); + void DecodeClientData(ITransportInbound data); + void OnDownstreamFinished(); + void OnTimerFired(string name); + void OnBodyMessage(object msg); + void Cleanup(); +} +``` + +- [ ] **Step 2: Update IServerStageOperations** + +Remove `TurboConnectionInfo` property (it becomes an `IHttpConnectionFeature` created by the stage logic). Change `OnRequest` parameter: + +```csharp +using Akka.Actor; +using Akka.Event; +using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Transport; +using TurboHTTP.Context.Features; + +namespace TurboHTTP.Streams.Stages.Server; + +internal interface IServerStageOperations +{ + void OnRequest(IFeatureCollection features); + void OnOutbound(ITransportOutbound item); + void OnScheduleTimer(string name, TimeSpan delay); + void OnCancelTimer(string name); + ILoggingAdapter Log { get; } + IActorRef StageActor { get; } + IMaterializer Materializer { get; } + IServiceProvider? Services => null; + IHttpConnectionFeature? ConnectionFeature => null; + TlsHandshakeFeature? TlsHandshakeFeature => null; +} +``` + +- [ ] **Step 3: Update ServerConnectionShape** + +Replace all `RequestContext` port types with `IFeatureCollection`: + +```csharp +using System.Collections.Immutable; +using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Transport; + +namespace TurboHTTP.Streams.Stages.Server; + +internal sealed class ServerConnectionShape : Shape +{ + public Inlet InNetwork { get; } + public Outlet OutRequest { get; } + public Inlet InResponse { get; } + public Outlet OutNetwork { get; } + + public ServerConnectionShape( + Inlet inNetwork, + Outlet outRequest, + Inlet inResponse, + Outlet outNetwork) + { + InNetwork = inNetwork; + OutRequest = outRequest; + InResponse = inResponse; + OutNetwork = outNetwork; + } + + public override ImmutableArray Inlets => [InNetwork, InResponse]; + + public override ImmutableArray Outlets => [OutRequest, OutNetwork]; + + public override Shape DeepCopy() + { + return new ServerConnectionShape( + (Inlet)InNetwork.CarbonCopy(), + (Outlet)OutRequest.CarbonCopy(), + (Inlet)InResponse.CarbonCopy(), + (Outlet)OutNetwork.CarbonCopy()); + } + + public override Shape CopyFromPorts(ImmutableArray inlets, ImmutableArray outlets) + { + return new ServerConnectionShape( + (Inlet)inlets[0], + (Outlet)outlets[0], + (Inlet)inlets[1], + (Outlet)outlets[1]); + } +} +``` + +- [ ] **Step 4: Update IServerProtocolEngine** + +```csharp +using Akka; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Transport; + +namespace TurboHTTP.Streams; + +internal interface IServerProtocolEngine +{ + BidiFlow CreateFlow( + IServiceProvider? services = null); +} +``` + +- [ ] **Step 5: Commit** + +``` +git add src/TurboHTTP/Protocol/IServerStateMachine.cs src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs src/TurboHTTP/Streams/IServerProtocolEngine.cs +git commit -m "refactor!: change core interfaces from RequestContext to IFeatureCollection" +``` + +--- + +## Task 4: Protocol Encoder + StreamState Changes + +Update all protocol encoders and H2 StreamState to accept `IFeatureCollection` instead of `RequestContext`. These are mechanical: every encoder already does `context.Features.Get()` — change the parameter name from `context` to `features` and remove the `.Features` indirection. + +**Files:** +- Modify: `src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs` + +- [ ] **Step 1: Update Http11ServerEncoder** + +Change `Encode` signature from `RequestContext context` to `IFeatureCollection features`. Replace all `context.Features.Get()` with `features.Get()`. Remove `using TurboHTTP.Streams.Stages.Server;`, add `using Microsoft.AspNetCore.Http.Features;` if not already present. + +The method signature becomes: +```csharp +public int Encode(Span destination, IFeatureCollection features, bool isChunked = false, bool connectionClose = false) +``` + +All internal access changes from `context.Features.Get()` to `features.Get()`. + +- [ ] **Step 2: Update Http2ServerEncoder** + +Change `EncodeHeaders` signature: +```csharp +public IReadOnlyList EncodeHeaders(IFeatureCollection features, int streamId, bool hasBody) +``` + +Change `BuildHeaderList` signature: +```csharp +private static void BuildHeaderList(IFeatureCollection features, List headers) +``` + +Replace all `context.Features.Get()` → `features.Get()`. + +- [ ] **Step 3: Update Http3ServerEncoder** + +Change `EncodeHeaders` signature: +```csharp +public HeadersFrame EncodeHeaders(IFeatureCollection features) +``` + +Change `BuildHeaderList` signature: +```csharp +private static void BuildHeaderList(IFeatureCollection features, List<(string Name, string Value)> headers) +``` + +Replace all `context.Features.Get()` → `features.Get()`. + +- [ ] **Step 4: Update Http2 StreamState** + +Replace the `RequestContext` field and methods: +- Change `private RequestContext? _requestContext;` to `private IFeatureCollection? _features;` +- Change `SetTurboContext(RequestContext context)` to `SetFeatures(IFeatureCollection features)` → `_features = features;` +- Change `GetTurboContext()` to `GetFeatures()` → `return _features;` +- Remove `using TurboHTTP.Streams.Stages.Server;`, add `using Microsoft.AspNetCore.Http.Features;` + +- [ ] **Step 5: Commit** + +``` +git add src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs +git commit -m "refactor: update protocol encoders to accept IFeatureCollection" +``` + +--- + +## Task 5: Protocol State Machines + Session Managers + +Update all `OnResponse` implementations and request-creation paths to use `IFeatureCollection` and `FeatureCollectionFactory`. + +**Files:** +- Modify: `src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs` +- Modify: `src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs` +- Modify: `src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs` + +- [ ] **Step 1: Update Http10ServerStateMachine** + +Two changes: +1. Request creation (in `DecodeClientData`): Replace `ServerContextFactory.Create(feature, hasBody, ...)` with `FeatureCollectionFactory.Create(feature, hasBody, ...)`. Change `_ops.OnRequest(context)` to `_ops.OnRequest(features)` (variable rename). +2. `OnResponse`: Change signature to `public void OnResponse(IFeatureCollection features)`. Replace `context.Features.Get()` → `features.Get()`. + +Update using: replace `using TurboHTTP.Streams.Stages.Server;` with nothing (no longer needed for RequestContext). Add `using Microsoft.AspNetCore.Http.Features;` if missing. Keep `using TurboHTTP.Server;` for FeatureCollectionFactory. + +- [ ] **Step 2: Update Http11ServerStateMachine** + +Same two changes: +1. Request creation (~line 139): `ServerContextFactory.Create(...)` → `FeatureCollectionFactory.Create(...)`, variable `context` → `features`. +2. `OnResponse` (~line 167): Change parameter to `IFeatureCollection features`. Replace all `context.Features.Get()` → `features.Get()`. The encoder call changes from `_encoder.Encode(span, context, ...)` to `_encoder.Encode(span, features, ...)`. + +- [ ] **Step 3: Update Http2ServerStateMachine + SessionManager** + +StateMachine: `public void OnResponse(IFeatureCollection features) => _sessionManager.OnResponse(features);` + +SessionManager `OnResponse` (~line 129): Change parameter to `IFeatureCollection features`. Replace `context.Features.Get()` → `features.Get()`. The `GetStreamIdFromContext(context)` call needs to change to `GetStreamIdFromFeatures(features)` (or inline: `features.Get()?.StreamId ?? -1`). + +SessionManager request creation (~line 541): Replace `ServerContextFactory.Create(...)` → `FeatureCollectionFactory.Create(...)`. Change `context.Features.Set(...)` → `features.Set(...)`. The StreamState call `state.SetTurboContext(context)` → `state.SetFeatures(features)`. + +The encoder call `_responseEncoder.EncodeHeaders(context, streamId, hasBody)` → `_responseEncoder.EncodeHeaders(features, streamId, hasBody)`. + +- [ ] **Step 4: Update Http3ServerStateMachine + SessionManager** + +Same pattern as H2: + +StateMachine: `public void OnResponse(IFeatureCollection features) => _sessionManager.OnResponse(features);` + +SessionManager: Same changes as H2 — parameter rename, `FeatureCollectionFactory.Create()`, `features.Get/Set`, encoder accepts `IFeatureCollection`. + +- [ ] **Step 5: Update ProtocolNegotiatingStateMachine** + +```csharp +public void OnResponse(IFeatureCollection features) => _inner!.OnResponse(features); +``` + +Remove `using TurboHTTP.Streams.Stages.Server;` if no longer needed. + +- [ ] **Step 6: Commit** + +``` +git add src/TurboHTTP/Protocol/ +git commit -m "refactor: update all state machines and session managers to IFeatureCollection" +``` + +--- + +## Task 6: HttpConnectionServerStageLogic + Connection Stages + Engines + +Update the core stage logic, all five connection stages, and all five engine classes. The connection stages and engines are mostly mechanical port-type changes. + +**Files:** +- Modify: `src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs` +- Modify: `src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs` +- Modify: `src/TurboHTTP/Streams/Http10ServerEngine.cs` +- Modify: `src/TurboHTTP/Streams/Http11ServerEngine.cs` +- Modify: `src/TurboHTTP/Streams/Http20ServerEngine.cs` +- Modify: `src/TurboHTTP/Streams/Http30ServerEngine.cs` +- Modify: `src/TurboHTTP/Streams/NegotiatingServerEngine.cs` + +- [ ] **Step 1: Update HttpConnectionServerStageLogic** + +Key changes: +1. Field types: `Outlet` → `Outlet`, `Inlet` → `Inlet`, `Queue` → `Queue` +2. `OnRequest(IFeatureCollection features)` implementation: same logic, different type +3. Response handler (`_inResponse` onPush): `var response = Grab(_inResponse);` now gives `IFeatureCollection`. `_sm.OnResponse(response)` already matches new interface. `response.Features.Get()` → `response.Get()` (no `.Features` needed). `ServerContextFactory.Return(response)` → `FeatureCollectionFactory.Return(response)`. +4. Connection info: The `_connectionInfo` field changes from `TurboConnectionInfo?` to `TurboHttpConnectionFeature?`. In `OnNetworkPush` where `TransportConnected` is handled, create `TurboHttpConnectionFeature` directly instead of `TurboConnectionInfo`. +5. Remove `using TurboHTTP.Server;` for TurboConnectionInfo. Add `using Microsoft.AspNetCore.Http.Features;`. + +The `IServerStageOperations.ConnectionInfo` property changes from `TurboConnectionInfo?` to `IHttpConnectionFeature?` — return `_connectionFeature`. + +- [ ] **Step 2: Update all five connection stages** + +Each connection stage defines four ports with explicit types. Update port declarations: + +```csharp +// Before +private readonly Outlet _outRequest = new("Http11.Request.Out"); +private readonly Inlet _inResponse = new("Http11.Response.In"); + +// After +private readonly Outlet _outRequest = new("Http11.Request.Out"); +private readonly Inlet _inResponse = new("Http11.Response.In"); +``` + +Add `using Microsoft.AspNetCore.Http.Features;`, remove `using TurboHTTP.Streams.Stages.Server;` if RequestContext was the only reason. + +Apply to: `Http10ServerConnectionStage`, `Http11ServerConnectionStage`, `Http20ServerConnectionStage`, `Http30ServerConnectionStage`, `ProtocolNegotiatorConnectionStage`. + +- [ ] **Step 3: Update all five engine classes** + +Each engine's `CreateFlow` method returns a `BidiFlow`. Change to `BidiFlow`. + +Apply to: `Http10ServerEngine`, `Http11ServerEngine`, `Http20ServerEngine`, `Http30ServerEngine`, `NegotiatingServerEngine`. + +Add `using Microsoft.AspNetCore.Http.Features;`, remove `using TurboHTTP.Streams.Stages.Server;` if no longer needed. + +- [ ] **Step 4: Commit** + +``` +git add src/TurboHTTP/Streams/ +git commit -m "refactor: update stage logic, connection stages, and engines to IFeatureCollection" +``` + +--- + +## Task 7: Rewrite ApplicationBridgeStage\ + +Full rewrite of ApplicationBridgeStage as a generic stage that directly holds `IHttpApplication`. Shape changes to `FlowShape`. + +**Files:** +- Rewrite: `src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs` + +- [ ] **Step 1: Write the new ApplicationBridgeStage\** + +```csharp +using Akka.Actor; +using Akka.Streams; +using Akka.Streams.Stage; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using TurboHTTP.Context.Features; + +namespace TurboHTTP.Streams.Stages.Server; + +internal sealed class ApplicationBridgeStage : GraphStage> + where TContext : notnull +{ + private readonly IHttpApplication _application; + private readonly int _parallelism; + private readonly TimeSpan _handlerTimeout; + private readonly TimeSpan _handlerGracePeriod; + + private readonly Inlet _in = new("AppBridge.In"); + private readonly Outlet _out = new("AppBridge.Out"); + + public override FlowShape Shape { get; } + + public ApplicationBridgeStage( + IHttpApplication application, + int parallelism, + TimeSpan handlerTimeout, + TimeSpan handlerGracePeriod) + { + _application = application; + _parallelism = parallelism; + _handlerTimeout = handlerTimeout; + _handlerGracePeriod = handlerGracePeriod; + Shape = new FlowShape(_in, _out); + } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + + private sealed record DispatchCompleted(int Sequence, IFeatureCollection Features); + + private sealed record DispatchFailed(int Sequence, IFeatureCollection Features, Exception Error); + + private sealed record ResponseReady(int Sequence, IFeatureCollection Features, Task HandlerTask); + + private sealed record HandlerFinished(int Sequence, IFeatureCollection Features); + + private sealed record HandlerFaulted(int Sequence, IFeatureCollection Features, Exception Error); + + private sealed record HandlerTimedOut(int Sequence, IFeatureCollection Features); + + private sealed class Logic : GraphStageLogic + { + private readonly ApplicationBridgeStage _stage; + private IActorRef? _stageActor; + private bool _upstreamFinished; + private int _inFlight; + private int _sequence; + private int _nextToEmit; + private bool _downstreamReady; + private readonly SortedDictionary _pending = []; + private readonly Dictionary _activeTimeouts = []; + private readonly Dictionary _appContexts = []; + + public Logic(ApplicationBridgeStage stage) : base(stage.Shape) + { + _stage = stage; + + SetHandler(stage._in, + onPush: OnPush, + onUpstreamFinish: () => + { + _upstreamFinished = true; + if (_inFlight == 0) + { + CompleteStage(); + } + }); + + SetHandler(stage._out, + onPull: () => + { + _downstreamReady = true; + TryEmitPending(); + TryPullNext(); + }); + } + + public override void PreStart() + { + _stageActor = GetStageActor(OnMessage).Ref; + Pull(_stage._in); + } + + private void OnPush() + { + var features = Grab(_stage._in); + var seq = _sequence++; + + _inFlight++; + + try + { + DispatchAsync(features, seq); + } + catch (Exception) + { + _inFlight--; + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + } + + TryPullNext(); + } + + private void DispatchAsync(IFeatureCollection features, int seq) + { + TContext appContext; + try + { + appContext = _stage._application.CreateContext(features); + _appContexts[seq] = appContext; + } + catch (Exception) + { + _inFlight--; + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + return; + } + + var task = _stage._application.ProcessRequestAsync(appContext); + + if (task.IsCompletedSuccessfully) + { + _inFlight--; + _stage._application.DisposeContext(appContext, null); + _appContexts.Remove(seq); + CompleteResponseBody(features); + Emit(seq, features); + } + else if (task.IsFaulted) + { + _inFlight--; + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + _stage._application.DisposeContext(appContext, task.Exception); + _appContexts.Remove(seq); + CompleteResponseBody(features); + Emit(seq, features); + } + else + { + var lifetime = features.Get(); + var cts = lifetime is not null + ? CancellationTokenSource.CreateLinkedTokenSource(lifetime.RequestAborted) + : new CancellationTokenSource(); + cts.CancelAfter(_stage._handlerTimeout); + _activeTimeouts[seq] = cts; + + var bodyFeature = features.Get() as TurboHttpResponseBodyFeature; + var headersReady = bodyFeature?.WhenHeadersReady; + + Task.Delay(_stage._handlerTimeout + _stage._handlerGracePeriod, cts.Token) + .PipeTo(_stageActor!, + success: () => new HandlerTimedOut(seq, features)); + + if (headersReady is not null) + { + Task.WhenAny(headersReady, task) + .PipeTo(_stageActor!, + success: () => new ResponseReady(seq, features, task)); + } + else + { + task.PipeTo(_stageActor!, + success: () => new DispatchCompleted(seq, features), + failure: ex => new DispatchFailed(seq, features, ex)); + } + } + } + + private void OnMessage((IActorRef sender, object msg) args) + { + switch (args.msg) + { + case ResponseReady(var seq, var features, var handlerTask): + if (handlerTask.IsFaulted) + { + if (features.Get() is not TurboHttpResponseBodyFeature + { + HasStarted: true + }) + { + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + } + } + + if (handlerTask.IsCompleted) + { + CompleteResponseBody(features); + _inFlight--; + DisposeCts(seq); + DisposeAppContext(seq, handlerTask.Exception); + Emit(seq, features); + } + else + { + Emit(seq, features); + handlerTask.PipeTo(_stageActor!, + success: () => new HandlerFinished(seq, features), + failure: ex => new HandlerFaulted(seq, features, ex)); + } + + break; + + case HandlerFinished(var seq, var finishedFeatures): + CompleteResponseBody(finishedFeatures); + _inFlight--; + DisposeCts(seq); + DisposeAppContext(seq, null); + if (_upstreamFinished && _inFlight == 0) + { + CompleteStage(); + } + + break; + + case HandlerFaulted(var seq, var faultedFeatures, var error): + CompleteResponseBody(faultedFeatures); + _inFlight--; + DisposeCts(seq); + DisposeAppContext(seq, error); + if (_upstreamFinished && _inFlight == 0) + { + CompleteStage(); + } + + break; + + case DispatchCompleted(var seq, var features): + _inFlight--; + DisposeCts(seq); + DisposeAppContext(seq, null); + CompleteResponseBody(features); + Emit(seq, features); + break; + + case DispatchFailed(var seq, var features, var error): + _inFlight--; + DisposeCts(seq); + DisposeAppContext(seq, error); + var respFeature = features.Get(); + if (respFeature is not null) + { + respFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + break; + + case HandlerTimedOut(var seq, var features): + if (_activeTimeouts.TryGetValue(seq, out var cts)) + { + cts.Dispose(); + _activeTimeouts.Remove(seq); + var respFeatureTimeout = features.Get(); + if (respFeatureTimeout is not null && respFeatureTimeout.StatusCode == 200) + { + respFeatureTimeout.StatusCode = 503; + CompleteResponseBody(features); + _inFlight--; + DisposeAppContext(seq, null); + Emit(seq, features); + } + } + + break; + } + + if (_upstreamFinished && _inFlight == 0 && _pending.Count == 0) + { + CompleteStage(); + } + } + + private void DisposeAppContext(int seq, Exception? exception) + { + if (_appContexts.TryGetValue(seq, out var appCtx)) + { + _stage._application.DisposeContext(appCtx, exception); + _appContexts.Remove(seq); + } + } + + private void DisposeCts(int seq) + { + if (_activeTimeouts.TryGetValue(seq, out var cts)) + { + cts.Dispose(); + _activeTimeouts.Remove(seq); + } + } + + private void TryPullNext() + { + if (_inFlight < _stage._parallelism && !HasBeenPulled(_stage._in)) + { + Pull(_stage._in); + } + } + + private void Emit(int seq, IFeatureCollection features) + { + _pending[seq] = features; + TryEmitPending(); + } + + private void TryEmitPending() + { + while (_downstreamReady && _pending.Count > 0 && _pending.Keys.First() == _nextToEmit) + { + _downstreamReady = false; + Push(_stage._out, _pending[_nextToEmit]); + _pending.Remove(_nextToEmit); + _nextToEmit++; + } + } + + private static void CompleteResponseBody(IFeatureCollection features) + { + var bodyFeature = features.Get() as TurboHttpResponseBodyFeature; + bodyFeature?.Complete(); + } + } +} +``` + +Key improvements over old version: +- Generic `` — no type erasure, `_appContexts` is `Dictionary` not `Dictionary` +- `IFeatureCollection` directly — no RequestContext wrapper +- Consolidated `DisposeAppContext` helper — reduces duplication +- Lifetime CTS from `IHttpRequestLifetimeFeature` — no `RequestContext.Lifetime` + +- [ ] **Step 2: Commit** + +``` +git add src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs +git commit -m "refactor!: rewrite ApplicationBridgeStage as generic with IHttpApplication" +``` + +--- + +## Task 8: Actor + Server Integration + +Update `ListenerActor`, `ConnectionActor`, and `TurboServer` to use `ApplicationBridgeStage` instead of `RoutingStage`. The actors receive the bridge flow instead of routing delegate + route table. + +**Files:** +- Modify: `src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs` +- Modify: `src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs` +- Modify: `src/TurboHTTP/Server/TurboServer.cs` + +- [ ] **Step 1: Update ConnectionActor** + +Change the `Materialize` record to receive a `Flow` instead of `TurboRequestDelegate` + `RouteTable`: + +```csharp +public sealed record Materialize( + Flow ConnectionFlow, + IServerProtocolEngine Engine, + Flow BridgeFlow, + IServiceProvider Services, + IMaterializer Materializer, + string? ConnectionLoggingCategory = null); +``` + +In `OnMaterialize`, replace the RoutingStage with the bridge flow: + +```csharp +private void OnMaterialize(Materialize msg) +{ + _log.Debug("Connection {0} materializing pipeline", _connectionId); + + _killSwitch = KillSwitches.Shared("connection-" + _connectionId); + + var protocolBidi = msg.Engine.CreateFlow(msg.Services); + var composed = protocolBidi.Join(msg.BridgeFlow); + + // ... rest of logging and pipeline assembly unchanged ... + + var completionTask = pipeline + .ViaMaterialized( + Flow.Create().WatchTermination(Keep.Right), + Keep.Right) + .Join(composed) + .Run(msg.Materializer); + + completionTask.PipeTo(self, + success: () => new StreamCompleted(null), + failure: ex => new StreamCompleted(ex)); +} +``` + +Remove `using TurboHTTP.Server;` for RouteTable/TurboRequestDelegate. Remove `using TurboHTTP.Streams.Stages.Server;` for RoutingStage. Add `using Microsoft.AspNetCore.Http.Features;`. + +- [ ] **Step 2: Update ListenerActor** + +Remove `TurboRequestDelegate` and `RouteTable` fields/params. Add `Flow` bridge flow: + +Constructor changes: +```csharp +public ListenerActor( + IListenerFactory factory, + ListenerOptions listenerOptions, + TurboServerOptions serverOptions, + Flow bridgeFlow, + IServiceProvider services, + IMaterializer materializer, + string? connectionLoggingCategory = null) +``` + +`Create` factory method: +```csharp +public static Props Create( + IListenerFactory factory, + ListenerOptions listenerOptions, + TurboServerOptions serverOptions, + Flow bridgeFlow, + IServiceProvider services, + IMaterializer materializer, + string? connectionLoggingCategory = null) + => Props.Create(() => new ListenerActor( + factory, listenerOptions, serverOptions, + bridgeFlow, services, materializer, + connectionLoggingCategory)); +``` + +In `OnIncomingConnection`, the `Materialize` message changes: +```csharp +child.Tell(new ConnectionActor.Materialize( + msg.ConnectionFlow, + engine, + _bridgeFlow, + _services, + _materializer, + _connectionLoggingCategory)); +``` + +- [ ] **Step 3: Update TurboServer** + +Wire `IHttpApplication` through to the bridge stage: + +```csharp +public async Task StartAsync( + IHttpApplication application, + CancellationToken cancellationToken) where TContext : notnull +{ + _system = _services.GetService(); + if (_system is null) + { + var setup = BootstrapSetup.Create() + .WithConfig(LoggingHocon) + .And(new LoggerFactorySetup(_loggerFactory)); + _system = ActorSystem.Create("turbo-server", setup); + _ownsSystem = true; + } + + var materializer = _system.Materializer(); + + // Parallelism controls max in-flight requests per connection in the bridge. + // Use H2 MaxConcurrentStreams as a reasonable default (100). + // H1.1 connections are sequential anyway; H2/H3 benefit from parallel dispatch. + var parallelism = _options.Http2.MaxConcurrentStreams; + var bridgeStage = new ApplicationBridgeStage( + application, + parallelism, + _options.HandlerTimeout, + _options.HandlerGracePeriod); + var bridgeFlow = Flow.FromGraph(bridgeStage); + + var resolver = new EndpointResolver(); + var resolvedEndpoints = resolver.Resolve(_options); + + var listenerProps = new List(resolvedEndpoints.Count); + foreach (var endpoint in resolvedEndpoints) + { + listenerProps.Add(ListenerActor.Create( + endpoint.Factory, + endpoint.Options, + _options, + bridgeFlow, + _services, + materializer, + endpoint.ConnectionLoggingCategory)); + } + + // ... rest unchanged (supervisor, coordinated shutdown) ... +} +``` + +Remove the dead-code `TurboRequestDelegate pipeline = _ => Task.CompletedTask;` and `new TurboRouteTable().Freeze()`. + +Note: If `TurboServerOptions.Limits.MaxConcurrentRequests` doesn't exist yet, use a sensible default (e.g., `_options.Http2.MaxConcurrentStreams` or hardcode `100`). Check what property is available. + +- [ ] **Step 4: Commit** + +``` +git add src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs src/TurboHTTP/Server/TurboServer.cs +git commit -m "refactor!: wire IHttpApplication through actors to ApplicationBridgeStage" +``` + +--- + +## Task 9: Delete Old Types + DI Cleanup + +Remove all types that are no longer referenced. Clean up DI registration. + +**Files:** +- Delete: `src/TurboHTTP/Streams/Stages/Server/RequestContext.cs` +- Delete: `src/TurboHTTP/Server/TurboHttpContext.cs` +- Delete: `src/TurboHTTP/Context/TurboHttpRequest.cs` +- Delete: `src/TurboHTTP/Context/TurboHttpResponse.cs` +- Delete: `src/TurboHTTP/Server/TurboConnectionInfo.cs` +- Delete: `src/TurboHTTP/Streams/Stages/Server/RoutingStage.cs` +- Delete: `src/TurboHTTP/Server/RouteTable.cs` +- Delete: `src/TurboHTTP/Server/TurboRequestDelegate.cs` +- Delete: `src/TurboHTTP/Server/ServerContextFactory.cs` +- Modify: `src/TurboHTTP/Server/TurboServerServiceCollectionExtensions.cs` + +- [ ] **Step 1: Delete all obsolete files** + +Delete each file listed above. Use `git rm` or filesystem delete. + +- [ ] **Step 2: Clean up TurboServerServiceCollectionExtensions** + +Remove any `RouteTable`-related registration. The `AddTurboKestrel` methods that registered `TurboRouteTable` should just register `IServer → TurboServer` and options. Check if there's a `TurboRouteTable` singleton registration to remove. + +Looking at the current code, `AddTurboKestrel` doesn't register RouteTable (TurboServer created it inline). No changes needed beyond verifying no compilation errors from deleted types. + +- [ ] **Step 3: Remove stale using directives** + +Grep for `using TurboHTTP.Streams.Stages.Server;` and `using TurboHTTP.Server;` across the codebase and remove any that now reference only deleted types. Key files to check: +- `src/TurboHTTP/Protocol/IProtocolSwitchCapable.cs` — may have unused using for RequestContext + +- [ ] **Step 4: Attempt compilation** + +Run: `dotnet build --configuration Release src/TurboHTTP.slnx` + +Fix any remaining compilation errors from missed references to deleted types. + +- [ ] **Step 5: Commit** + +``` +git add -A +git commit -m "refactor!: delete RequestContext, TurboHttpContext, RoutingStage, and all custom routing types" +``` + +--- + +## Task 10: Test Helper Updates + +Update the shared test helpers to work with `IFeatureCollection` instead of `RequestContext`. + +**Files:** +- Modify: `src/TurboHTTP.Tests.Shared/FakeServerOps.cs` +- Modify: `src/TurboHTTP.Tests.Shared/ServerTestContext.cs` +- Modify: `src/TurboHTTP.Tests.Shared/ServerTestContextBuilder.cs` + +- [ ] **Step 1: Update FakeServerOps** + +```csharp +using Akka.Actor; +using Akka.Event; +using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Transport; +using TurboHTTP.Streams.Stages.Server; + +namespace TurboHTTP.Tests.Shared; + +internal sealed class FakeServerOps : IServerStageOperations +{ + private readonly List _features = []; + + public List Requests => _features; + public List Outbound { get; } = []; + public List<(string Name, TimeSpan Delay)> ScheduledTimers { get; } = []; + public List CancelledTimers { get; } = []; + + public void OnRequest(IFeatureCollection features) => _features.Add(features); + public void OnOutbound(ITransportOutbound item) => Outbound.Add(item); + + public void OnScheduleTimer(string name, TimeSpan delay) + { + ScheduledTimers.RemoveAll(t => t.Name == name); + ScheduledTimers.Add((name, delay)); + } + + public void OnCancelTimer(string name) + { + ScheduledTimers.RemoveAll(t => t.Name == name); + CancelledTimers.Add(name); + } + + public ILoggingAdapter Log => NoLogger.Instance; + public IActorRef StageActor { get; set; } = ActorRefs.Nobody; + public IMaterializer Materializer { get; set; } = null!; +} +``` + +- [ ] **Step 2: Update ServerTestContext** + +```csharp +using Microsoft.AspNetCore.Http.Features; +using TurboHTTP.Context.Features; + +namespace TurboHTTP.Tests.Shared; + +internal static class ServerTestContext +{ + internal static ServerTestContextBuilder Request() => new(); + + internal static IFeatureCollection CreateResponse(int statusCode = 200) + { + var features = new TurboFeatureCollection(); + features.Set(new TurboHttpRequestFeature()); + var responseFeature = new TurboHttpResponseFeature { StatusCode = statusCode }; + features.Set(responseFeature); + var bodyFeature = new TurboHttpResponseBodyFeature(); + features.Set(bodyFeature); + return features; + } + + internal static IFeatureCollection CreateH2Response(int streamId, int statusCode = 200) + { + var features = CreateResponse(statusCode); + features.Set(new TurboStreamIdFeature(streamId)); + return features; + } + + internal static IFeatureCollection CreateH3Response(long streamId, int statusCode = 200) + { + var features = CreateResponse(statusCode); + features.Set(new TurboStreamIdFeature(streamId)); + return features; + } +} +``` + +- [ ] **Step 3: Update ServerTestContextBuilder** + +Change `Build()` to return `IFeatureCollection` instead of `RequestContext`. Remove `TurboConnectionInfo` creation — create `TurboHttpConnectionFeature` directly with the connection data: + +```csharp +public IFeatureCollection Build() +{ + var features = new TurboFeatureCollection(); + var requestFeature = BuildRequestFeature(); + features.Set(requestFeature); + var requestBodyFeature = new TurboRequestBodyFeature + { + Body = requestFeature.Body, + BodySource = _bodySource ?? Source.Empty>() + }; + features.Set(new TurboHttpResponseFeature()); + + if (_connection is not null) + { + features.Set(_connection); + } + + var bodyFeature = new TurboHttpResponseBodyFeature(); + features.Set(bodyFeature); + + var lifetimeFeature = new TurboHttpRequestLifetimeFeature(); + if (_cancellationToken != CancellationToken.None) + { + lifetimeFeature.RequestAborted = _cancellationToken; + } + features.Set(lifetimeFeature); + + return features; +} +``` + +Change `Connection(TurboConnectionInfo)` to `Connection(IHttpConnectionFeature)` — update the field type from `TurboConnectionInfo?` to `IHttpConnectionFeature?`. + +Remove usings for deleted types. + +- [ ] **Step 4: Commit** + +``` +git add src/TurboHTTP.Tests.Shared/ +git commit -m "refactor: update test helpers to use IFeatureCollection" +``` + +--- + +## Task 11: Unit Test Updates + +Update all state machine test specs that call `OnResponse(RequestContext)` or construct `RequestContext`. These tests use `ServerTestContext.CreateResponse()` and `FakeServerOps.Requests` — both now return/hold `IFeatureCollection`. + +**Files:** All state machine spec files in `src/TurboHTTP.Tests/Protocol/` + +- [ ] **Step 1: Bulk-update OnResponse calls in test files** + +The test pattern changes from: +```csharp +var response = ServerTestContext.CreateResponse(200); +sm.OnResponse(response); +``` +to the same code — the return type changed but the call site is identical since `ServerTestContext.CreateResponse()` now returns `IFeatureCollection`. + +The main change needed: where tests access `response.Features.Get()`, change to `response.Get()` (no `.Features` property on `IFeatureCollection`). + +Grep across `src/TurboHTTP.Tests/` for `\.Features\.Get` and `\.Features\.Set` to find all sites that need updating. + +Also grep for `new RequestContext` — these direct constructions need to change to creating `TurboFeatureCollection` directly. + +- [ ] **Step 2: Update ServerContextFactorySpec** + +Rename file to `FeatureCollectionFactorySpec.cs`. Change all `ServerContextFactory.Create(...)` → `FeatureCollectionFactory.Create(...)` and `ServerContextFactory.Return(...)` → `FeatureCollectionFactory.Return(...)`. The return type changes from `RequestContext` to `IFeatureCollection`, so `ctx.Features.Get()` → `features.Get()`. + +- [ ] **Step 3: Update ContextPoolingSpec** + +Same pattern: `ServerContextFactory.Create/Return` → `FeatureCollectionFactory.Create/Return`. Return types are `IFeatureCollection`. + +- [ ] **Step 4: Remove usings for deleted types** + +Grep `src/TurboHTTP.Tests/` for `using TurboHTTP.Streams.Stages.Server;` and remove where the only usage was `RequestContext`. Same for `using TurboHTTP.Server;` where only `TurboConnectionInfo` was used. + +- [ ] **Step 5: Attempt test compilation** + +Run: `dotnet build src/TurboHTTP.Tests/TurboHTTP.Tests.csproj` + +Fix any remaining compilation errors. + +- [ ] **Step 6: Run tests** + +Run: `dotnet run --project src/TurboHTTP.Tests/TurboHTTP.Tests.csproj` + +All existing tests should pass since the behavioral logic is unchanged — only the carrier type changed. + +- [ ] **Step 7: Commit** + +``` +git add src/TurboHTTP.Tests/ +git commit -m "refactor: update all unit tests to use IFeatureCollection" +``` + +--- + +## Task 12: Integration Test + API Surface Cleanup + +Update integration tests (which use RouteTable/TurboRequestDelegate) and the API surface verification test. Integration tests need to be updated to use ASP.NET's `IHttpApplication` pattern or temporarily disabled. + +**Files:** +- Modify: `src/TurboHTTP.IntegrationTests.Server/Shared/ServerSpecBase.cs` +- Modify: Integration test specs that use `ConfigureRoutes` +- Modify: `src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt` +- Modify: `src/TurboHTTP.Tests/Streams/Stages/Lifecycle/ListenerActorConnectionLimitSpec.cs` + +- [ ] **Step 1: Assess integration test scope** + +The integration tests use `ServerSpecBase` which configures routes via `TurboRouteTable`. Since we deleted `RouteTable` and `TurboRequestDelegate`, these tests need to be rewritten to use ASP.NET's `WebApplication` or `IHost` pattern with `TurboServer` as the `IServer`. + +This is a significant rewrite. Read `ServerSpecBase.cs` to understand the current pattern, then decide: rewrite now or mark as `[Fact(Skip = "Pending IServer integration")]`. + +Given the scope, the recommended approach is to **temporarily skip** integration tests with a clear skip reason, then fix them in a follow-up task. The unit tests validate the protocol layer; integration tests validate end-to-end with a real ASP.NET host. + +- [ ] **Step 2: Update ListenerActorConnectionLimitSpec** + +This unit test constructs `ListenerActor` with the old signature. Update to match the new constructor (bridge flow instead of routing delegate + route table). + +- [ ] **Step 3: Update API surface verification** + +The `CoreAPISpec.ApproveCore.DotNet.verified.txt` file lists the public API. Deleted public types (`TurboHttpContext`, `TurboHttpRequest`, `TurboHttpResponse`, `TurboConnectionInfo`, `TurboRequestDelegate`, `RouteTable`) must be removed from the verified file. + +Run: `dotnet run --project src/TurboHTTP.API.Tests/TurboHTTP.API.Tests.csproj` to regenerate the verified file, then approve changes. + +- [ ] **Step 4: Full build + test** + +``` +dotnet build --configuration Release src/TurboHTTP.slnx +dotnet run --project src/TurboHTTP.Tests/TurboHTTP.Tests.csproj +``` + +- [ ] **Step 5: Commit** + +``` +git add -A +git commit -m "refactor: update integration tests and API surface for IServer pipeline" +``` + +--- + +## Task 13: Documentation + CLAUDE.md Update + +Update CLAUDE.md to reflect the new architecture. Remove references to deleted types. + +**Files:** +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Update Architecture section in CLAUDE.md** + +Remove: +- `Context` line referencing `TurboHttpRequest, TurboHttpResponse, Adapters, Features` — change to just `Context (TurboHTTP/Context/) - Features/ (IHttp*Feature implementations), Adapters/` +- `Routing` line — delete entirely + +Remove from Build & Test: +- Any integration test commands that reference deleted features + +Update Code Style if any rules reference `TurboHttpContext`. + +- [ ] **Step 2: Commit** + +``` +git add CLAUDE.md +git commit -m "docs: update CLAUDE.md for IServer pipeline architecture" +``` + +--- + +## Parallelism Map + +Tasks that can run in parallel (after their dependencies complete): + +``` +Task 1 (features) ──┐ + ├── Task 2 (factory) ──┐ +Task 3 (interfaces) ┤ │ + ├── Task 4 (encoders) ─┤ + │ ├── Task 5 (state machines) + ├── Task 6 (stages) ───┤ + │ ├── Task 7 (bridge stage) + │ │ + └──────────────────────┴── Task 8 (actors + server) ── Task 9 (delete) ── Task 10 (test helpers) ── Task 11 (unit tests) ── Task 12 (integration) ── Task 13 (docs) +``` + +**Independent groups after Task 3:** +- Tasks 4+5 (protocol layer) +- Task 6 (stage layer) +- Task 7 (bridge stage) + +These three groups can be dispatched in parallel. Task 8 depends on all three completing. diff --git a/docs/superpowers/specs/2026-05-27-iserver-pipeline-redesign.md b/docs/superpowers/specs/2026-05-27-iserver-pipeline-redesign.md new file mode 100644 index 000000000..5fa759b46 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-iserver-pipeline-redesign.md @@ -0,0 +1,226 @@ +# TurboHTTP IServer Pipeline Redesign + +## Summary + +Strip TurboHTTP down to a pure transport+protocol layer. Remove all custom routing, context types, and middleware abstractions. ASP.NET's `IHttpApplication` becomes the sole request-handling contract. The Akka Streams pipeline carries `IFeatureCollection` directly — no wrapper types. + +## Goals + +- TurboHTTP is a drop-in `IServer` replacement for Kestrel +- ASP.NET Middleware, Controllers, Minimal APIs run natively on TurboHTTP +- No custom routing, no custom context, no custom pipeline delegate +- `IFeatureCollection` is the only data contract between protocol layer and application layer +- Parallel request dispatch with sequence ordering (H2/H3 multiplexing) + +## Non-Goals + +- Custom TurboHTTP routing or middleware system +- TurboHTTP-specific handler APIs +- Standalone server mode (without ASP.NET hosting) + +--- + +## Architecture + +### Stream Element: `IFeatureCollection` + +The Akka Streams pipeline element changes from `RequestContext` to `IFeatureCollection`. + +**Before:** +``` +Protocol Decoder → RequestContext → RoutingStage → TurboHttpContext → Handler +``` + +**After:** +``` +Protocol Decoder → IFeatureCollection → ApplicationBridgeStage → IFeatureCollection → Response Encoder +``` + +No wrapper, no intermediate context type. The feature collection IS the request. + +### ApplicationBridgeStage\ + +Generic Akka `GraphStage>` that bridges Akka Streams to ASP.NET's `IHttpApplication`. + +```csharp +internal sealed class ApplicationBridgeStage + : GraphStage> + where TContext : notnull +{ + private readonly IHttpApplication _application; + private readonly int _parallelism; + private readonly TimeSpan _handlerTimeout; + private readonly TimeSpan _handlerGracePeriod; +} +``` + +**Key properties:** +- Holds `IHttpApplication` directly — no type erasure, no adapter, no `Func` +- The generic parameter is captured in `TurboServer.StartAsync` and flows into the stage +- Stage instance is created once, shared across connection materializations (safe because `IHttpApplication` calls are per-request) + +**Parallel dispatch with sequence ordering:** +- Maintains `_inFlight` counter, pulls up to `_parallelism` concurrent requests +- `SortedDictionary` reorders completed requests for sequential emission +- Each request gets a sequence number on arrival, emitted in order regardless of completion order + +**Timeout management:** +- Per-request `CancellationTokenSource` linked to the request's `IHttpRequestLifetimeFeature` +- `CancelAfter(_handlerTimeout)` on the CTS +- Grace period via `Task.Delay(_handlerTimeout + _handlerGracePeriod)` → `HandlerTimedOut` message +- If timeout fires and no response headers sent: 503 + +**Request lifecycle in the stage:** + +``` +OnPush(features): + seq = _sequence++ + appContext = _application.CreateContext(features) + task = _application.ProcessRequestAsync(appContext) + + if task.IsCompletedSuccessfully: + _application.DisposeContext(appContext, null) + CompleteResponseBody(features) + Emit(seq, features) + + elif task.IsFaulted: + Set 500 on IHttpResponseFeature + _application.DisposeContext(appContext, task.Exception) + CompleteResponseBody(features) + Emit(seq, features) + + else (async): + Start timeout CTS + PipeTo(stageActor) for completion/failure/timeout signals + TryPullNext() for parallel dispatch +``` + +**Estimated size:** ~200-250 lines (down from 387). + +### FeatureCollectionFactory (renamed from ServerContextFactory) + +Pools `TurboFeatureCollection` instances with thread-static stack (max 32). + +```csharp +internal static class FeatureCollectionFactory +{ + public static IFeatureCollection Create( + IHttpRequestFeature requestFeature, + IHttpResponseFeature responseFeature, + IHttpResponseBodyFeature bodyFeature, + IHttpConnectionFeature connectionFeature, + IHttpRequestLifetimeFeature lifetimeFeature, + IHttpRequestIdentifierFeature identifierFeature, + ...); + + public static void Return(IFeatureCollection features); +} +``` + +- `Create()`: pops from pool or allocates, sets all features +- `Return()`: clears all features, disposes CTS from lifetime feature, pushes to pool +- CTS lifecycle: created by the protocol decoder, set as `IHttpRequestLifetimeFeature`, disposed on return + +### TurboServer Changes + +```csharp +public async Task StartAsync( + IHttpApplication application, + CancellationToken cancellationToken) where TContext : notnull +{ + // ActorSystem setup (unchanged) + + var bridgeStage = new ApplicationBridgeStage( + application, + _options.MaxConcurrentRequests, + _options.HandlerTimeout, + _options.HandlerGracePeriod); + + // Resolve endpoints, create listeners with bridgeStage + // No TurboRequestDelegate, no RouteTable +} +``` + +### HttpConnectionServerStageLogic Changes + +Port types change: +- `Outlet` → `Outlet` +- `Inlet` → `Inlet` +- `IServerStageOperations.OnRequest(RequestContext)` → `OnRequest(IFeatureCollection)` + +Protocol decoders create features → `FeatureCollectionFactory.Create(...)` → push `IFeatureCollection` directly. + +### ServerConnectionShape Changes + +The shape definition updates its port types from `RequestContext` to `IFeatureCollection`. + +--- + +## Deletions + +### Types to Delete + +| Type | File | Reason | +|------|------|--------| +| `RequestContext` | `Streams/Stages/Server/RequestContext.cs` | Replaced by `IFeatureCollection` as stream element | +| `TurboHttpContext` | `Server/TurboHttpContext.cs` | ASP.NET builds its own `HttpContext` | +| `TurboHttpRequest` | `Context/TurboHttpRequest.cs` | No consumer without TurboHttpContext | +| `TurboHttpResponse` | `Context/TurboHttpResponse.cs` | No consumer without TurboHttpContext | +| `TurboConnectionInfo` | `Server/TurboConnectionInfo.cs` | ASP.NET has `IHttpConnectionFeature` | +| `RoutingStage` | `Streams/Stages/Server/RoutingStage.cs` | No custom routing | +| `RouteTable` | `Server/RouteTable.cs` | No custom routing | +| `RouteMatchResult` | `Routing/RouteMatchResult.cs` | No custom routing | +| `TurboRequestDelegate` | `Server/TurboRequestDelegate.cs` | No custom pipeline | +| `Routing/` folder | `Routing/**` | All dispatchers, binding, route types | + +### Tests to Delete + +All tests for deleted types: +- `ContextPoolingSpec.cs` (tests RequestContext pooling) +- Any tests for RoutingStage, RouteTable, TurboHttpContext +- Tests for TurboHttpRequest/TurboHttpResponse standalone usage + +### Tests to Modify + +- `ServerContextFactorySpec.cs` → rename to `FeatureCollectionFactorySpec.cs`, test IFeatureCollection pooling +- Integration tests that construct TurboHttpContext manually → use IFeatureCollection directly + +--- + +## DI Registration Changes + +`TurboServerServiceCollectionExtensions.cs`: +- `AddTurboServer()` stays (registers `IServer` → `TurboServer`) +- `AddTurboKestrel()` removes `TurboRouteTable` registration, removes any routing-related DI + +--- + +## Data Flow (Final) + +``` +Network Bytes (TCP/TLS/QUIC) + → TransportFlow (Servus.Akka) + → ProtocolEngine (Http11/H2/H3 decoder) + → FeatureCollectionFactory.Create(requestFeature, responseFeature, ...) + → [IFeatureCollection] pushed to outlet + + → ApplicationBridgeStage + → _application.CreateContext(features) + → _application.ProcessRequestAsync(appContext) + → _application.DisposeContext(appContext, exception) + → CompleteResponseBody(features) + → [IFeatureCollection] emitted downstream + + → HttpConnectionServerStageLogic (response inlet) + → Protocol encoder writes response bytes + → FeatureCollectionFactory.Return(features) + → Network Bytes +``` + +--- + +## Migration Notes + +- The `ApplicationBridgeStage` file gets rewritten, not modified — the current implementation has type-erased `Func` that is replaced by generic `IHttpApplication` +- `ListenerActor` and `ConnectionActor` constructor signatures change (no more `TurboRequestDelegate`/`RouteTable` params, gain `ApplicationBridgeStage` or the graph flow) +- Protocol state machines (`Http11ServerStateMachine`, `Http2ServerSessionManager`, etc.) change their `OnRequest` callback to emit `IFeatureCollection` instead of `RequestContext` diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e050d27bc..8fc1d88c0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -8,16 +8,16 @@ - - + + - - + + @@ -28,4 +28,7 @@ + + + \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore.Tests/AkkaServerSentEventResultSpec.cs b/src/Servus.Akka.AspNetCore.Tests/AkkaServerSentEventResultSpec.cs new file mode 100644 index 000000000..ced0f78de --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/AkkaServerSentEventResultSpec.cs @@ -0,0 +1,119 @@ +using System.Text; +using Akka.Actor; +using Akka.Streams; +using Akka.Streams.Dsl; +using Akka.TestKit.Xunit; +using Microsoft.AspNetCore.Http; +using Servus.Akka.Sse; + +namespace Servus.Akka.AspNetCore.Tests; + +public sealed class AkkaServerSentEventResultSpec : TestKit +{ + private readonly IMaterializer _materializer; + + public AkkaServerSentEventResultSpec() : base(ActorSystem.Create("test")) + { + _materializer = Sys.Materializer(); + } + + private static (DefaultHttpContext Context, MemoryStream Body) CreateTestContext() + { + var body = new MemoryStream(); + var ctx = new DefaultHttpContext + { + Response = + { + Body = body + } + }; + return (ctx, body); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_set_content_type_to_event_stream() + { + var source = Source.From([new ServerSentEvent("hello")]); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, _) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal("text/event-stream", ctx.Response.ContentType); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_set_status_200() + { + var source = Source.From([new ServerSentEvent("hello")]); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, _) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal(200, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_format_single_event() + { + var source = Source.From([new ServerSentEvent("hello")]); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + body.Position = 0; + var content = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("data: hello\n\n", content); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_format_multiple_events() + { + var events = new[] + { + new ServerSentEvent("first"), + new ServerSentEvent("second") + }; + var source = Source.From(events); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + body.Position = 0; + var content = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("data: first\n\ndata: second\n\n", content); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_format_event_with_type_and_id() + { + var source = Source.From([ + new ServerSentEvent("payload", EventType: "update", Id: "42") + ]); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + body.Position = 0; + var content = Encoding.UTF8.GetString(body.ToArray()); + Assert.Contains("event: update\n", content); + Assert.Contains("data: payload\n", content); + Assert.Contains("id: 42\n", content); + } + + [Fact(Timeout = 5000)] + public async Task Sse_should_handle_empty_source() + { + var source = Source.Empty(); + var result = AkkaResults.ServerSentEvent(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal(0, body.Length); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore.Tests/AkkaStreamResultSpec.cs b/src/Servus.Akka.AspNetCore.Tests/AkkaStreamResultSpec.cs new file mode 100644 index 000000000..1f35d3715 --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/AkkaStreamResultSpec.cs @@ -0,0 +1,111 @@ +using System.Text; +using Akka.Actor; +using Akka.Streams; +using Akka.Streams.Dsl; +using Akka.TestKit.Xunit; +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore.Tests; + +public sealed class AkkaStreamResultSpec : TestKit +{ + private readonly IMaterializer _materializer; + + public AkkaStreamResultSpec() : base(ActorSystem.Create("test")) + { + _materializer = Sys.Materializer(); + } + + private static (DefaultHttpContext Context, MemoryStream Body) CreateTestContext() + { + var body = new MemoryStream(); + var ctx = new DefaultHttpContext + { + Response = + { + Body = body + } + }; + return (ctx, body); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_write_all_chunks_to_response_body() + { + var chunks = new[] + { + (ReadOnlyMemory)"hello "u8.ToArray(), + (ReadOnlyMemory)"world"u8.ToArray() + }; + var source = Source.From(chunks); + var result = AkkaResults.Stream(source, _materializer, "text/plain"); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + body.Position = 0; + var content = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("hello world", content); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_set_content_type() + { + var source = Source.From([(ReadOnlyMemory)new byte[] { 1 }]); + var result = AkkaResults.Stream(source, _materializer, "application/pdf"); + + var (ctx, _) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal("application/pdf", ctx.Response.ContentType); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_set_status_200() + { + var source = Source.From([(ReadOnlyMemory)new byte[] { 1 }]); + var result = AkkaResults.Stream(source, _materializer); + + var (ctx, _) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal(200, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_default_to_octet_stream_content_type() + { + var source = Source.From([(ReadOnlyMemory)new byte[] { 1 }]); + var result = AkkaResults.Stream(source, _materializer); + + var (ctx, _) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal("application/octet-stream", ctx.Response.ContentType); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_handle_empty_source() + { + var source = Source.Empty>(); + var result = AkkaResults.Stream(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal(0, body.Length); + } + + [Fact(Timeout = 5000)] + public async Task Stream_should_write_binary_data_correctly() + { + var data = new byte[] { 0x00, 0xFF, 0xAB, 0xCD }; + var source = Source.Single((ReadOnlyMemory)data); + var result = AkkaResults.Stream(source, _materializer); + + var (ctx, body) = CreateTestContext(); + await result.ExecuteAsync(ctx); + + Assert.Equal(data, body.ToArray()); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore.Tests/EntityBuilderSpec.cs b/src/Servus.Akka.AspNetCore.Tests/EntityBuilderSpec.cs new file mode 100644 index 000000000..a4f5d3523 --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/EntityBuilderSpec.cs @@ -0,0 +1,168 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Servus.Akka.AspNetCore.Tests; + +public sealed class EntityBuilderSpec +{ + [Fact(Timeout = 5000)] + public void OnGet_should_register_GET_method() + { + var builder = new EntityBuilder(); + builder.OnGet(() => new object()); + + Assert.True(builder.Methods.ContainsKey("GET")); + } + + [Fact(Timeout = 5000)] + public void OnPost_should_register_POST_method() + { + var builder = new EntityBuilder(); + builder.OnPost(() => new object()); + + Assert.True(builder.Methods.ContainsKey("POST")); + } + + [Fact(Timeout = 5000)] + public void OnPut_should_register_PUT_method() + { + var builder = new EntityBuilder(); + builder.OnPut(() => new object()); + + Assert.True(builder.Methods.ContainsKey("PUT")); + } + + [Fact(Timeout = 5000)] + public void OnDelete_should_register_DELETE_method() + { + var builder = new EntityBuilder(); + builder.OnDelete(() => new object()); + + Assert.True(builder.Methods.ContainsKey("DELETE")); + } + + [Fact(Timeout = 5000)] + public void OnPatch_should_register_PATCH_method() + { + var builder = new EntityBuilder(); + builder.OnPatch(() => new object()); + + Assert.True(builder.Methods.ContainsKey("PATCH")); + } + + [Fact(Timeout = 5000)] + public void Multiple_methods_should_register_independently() + { + var builder = new EntityBuilder(); + builder.OnGet(() => new object()); + builder.OnPost(() => new object()); + builder.OnDelete(() => new object()); + + Assert.Equal(3, builder.Methods.Count); + Assert.True(builder.Methods.ContainsKey("GET")); + Assert.True(builder.Methods.ContainsKey("POST")); + Assert.True(builder.Methods.ContainsKey("DELETE")); + } + + [Fact(Timeout = 5000)] + public void WithTimeout_should_set_timeout() + { + var builder = new EntityBuilder(); + builder.WithTimeout(TimeSpan.FromSeconds(30)); + + Assert.Equal(TimeSpan.FromSeconds(30), builder.Timeout); + } + + [Fact(Timeout = 5000)] + public void Default_timeout_should_be_5_seconds() + { + var builder = new EntityBuilder(); + + Assert.Equal(TimeSpan.FromSeconds(5), builder.Timeout); + } + + [Fact(Timeout = 5000)] + public void WithTimeout_should_be_fluent() + { + var builder = new EntityBuilder(); + var result = builder.WithTimeout(TimeSpan.FromSeconds(10)); + + Assert.Same(builder, result); + } + + [Fact(Timeout = 5000)] + public void Ask_should_configure_method_as_ask() + { + var builder = new EntityBuilder(); + builder.OnGet(() => new object()).Ask(ask => + { + ask.Handle(async (ctx, resp) => + { + await ctx.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(resp)); + }); + }); + + var config = builder.Methods["GET"].ToConfig(); + Assert.False(config.IsTell); + Assert.NotNull(config.EndpointMappers); + } + + [Fact(Timeout = 5000)] + public void Tell_should_configure_method_as_tell() + { + var builder = new EntityBuilder(); + builder.OnPost(() => new object()).Tell(tell => { tell.Produces(HttpStatusCode.Accepted); }); + + var config = builder.Methods["POST"].ToConfig(); + Assert.True(config.IsTell); + Assert.NotNull(config.TellResponseHandler); + } + + [Fact(Timeout = 5000)] + public void Tell_without_config_should_default_to_no_handler() + { + var builder = new EntityBuilder(); + builder.OnPost(() => new object()).Tell(); + + var config = builder.Methods["POST"].ToConfig(); + Assert.True(config.IsTell); + Assert.Null(config.TellResponseHandler); + } + + [Fact(Timeout = 5000)] + public void Response_should_add_mapper_to_builder() + { + var builder = new EntityBuilder(); + builder.Response(async (ctx, resp) => + { + await ctx.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(resp)); + }); + + Assert.Equal(1, builder.ResponseMappers.Count); + } + + [Fact(Timeout = 5000)] + public void Response_should_be_fluent() + { + var builder = new EntityBuilder(); + var result = builder.Response(async (ctx, resp) => + { + await ctx.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(resp)); + }); + + Assert.Same(builder, result); + } + + [Fact(Timeout = 5000)] + public void Methods_should_case_insensitive() + { + var builder = new EntityBuilder(); + builder.OnGet(() => new object()); + + Assert.True(builder.Methods.ContainsKey("get")); + Assert.True(builder.Methods.ContainsKey("GET")); + Assert.True(builder.Methods.ContainsKey("Get")); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore.Tests/EntityDispatcherSpec.cs b/src/Servus.Akka.AspNetCore.Tests/EntityDispatcherSpec.cs new file mode 100644 index 000000000..7cb6476ac --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/EntityDispatcherSpec.cs @@ -0,0 +1,290 @@ +using Akka.Actor; +using Akka.TestKit.Xunit; +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore.Tests; + +public sealed class EntityDispatcherSpec() : TestKit(ActorSystem.Create("test")) +{ + private sealed record TestMessage(string Value); + + private sealed record TestResponse(string Result); + + private sealed class EchoActor : ReceiveActor + { + public EchoActor() + { + Receive(msg => Sender.Tell(new TestResponse(msg.Value))); + } + } + + private sealed class TestResolver(IActorRef actor) : IEntityActorResolver + { + public ValueTask ResolveAsync(IServiceProvider services, CancellationToken cancellationToken) + { + return ValueTask.FromResult(actor); + } + } + + private static DefaultHttpContext CreateTestContext() + { + var ctx = new DefaultHttpContext + { + Response = + { + Body = new MemoryStream() + } + }; + return ctx; + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_dispatch_message_and_map_response() + { + var actor = Sys.ActorOf(Props.Create()); + var responseMappers = new EntityResponseMapperCollection(); + responseMappers.Add(async (ctx, resp) => + { + ctx.Response.StatusCode = 200; + await ctx.Response.WriteAsync(resp.Result); + }); + + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("hello")), + IsTell: false, + TimeoutOverride: null, + EndpointMappers: responseMappers, + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(actor)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("hello")); + + Assert.Equal(200, ctx.Response.StatusCode); + ctx.Response.Body.Position = 0; + using var reader = new StreamReader(ctx.Response.Body); + var body = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); + Assert.Equal("hello", body); + } + + [Fact(Timeout = 5000)] + public async Task Tell_should_send_message_and_return_202() + { + var probe = CreateTestProbe(); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("fire")), + IsTell: true, + TimeoutOverride: null, + EndpointMappers: null, + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(probe.Ref)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("fire")); + + Assert.Equal(202, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Tell_should_use_custom_status_code() + { + var probe = CreateTestProbe(); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("fire")), + IsTell: true, + TimeoutOverride: null, + EndpointMappers: null, + TellResponseHandler: ctx => + { + ctx.Response.StatusCode = 204; + return Task.CompletedTask; + }); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(probe.Ref)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("fire")); + + Assert.Equal(204, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_return_504_on_timeout() + { + var blackhole = Sys.ActorOf(Props.Create(() => new BlackholeActor())); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("timeout")), + IsTell: false, + TimeoutOverride: TimeSpan.FromMilliseconds(100), + EndpointMappers: new EntityResponseMapperCollection(), + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(blackhole)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("timeout")); + + Assert.Equal(504, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_return_500_when_no_mapper_found() + { + var actor = Sys.ActorOf(Props.Create()); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("hello")), + IsTell: false, + TimeoutOverride: null, + EndpointMappers: new EntityResponseMapperCollection(), + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(actor)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("hello")); + + Assert.Equal(500, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_use_endpoint_mappers_first() + { + var actor = Sys.ActorOf(Props.Create()); + var endpointMappers = new EntityResponseMapperCollection(); + var globalMappers = new EntityResponseMapperCollection(); + + endpointMappers.Add(async (ctx, _) => + { + ctx.Response.StatusCode = 201; + await ctx.Response.WriteAsync("endpoint"); + }); + + globalMappers.Add(async (ctx, _) => + { + ctx.Response.StatusCode = 200; + await ctx.Response.WriteAsync("global"); + }); + + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("hello")), + IsTell: false, + TimeoutOverride: null, + EndpointMappers: endpointMappers, + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, globalMappers, + TimeSpan.FromSeconds(5), new TestResolver(actor)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("hello")); + + Assert.Equal(201, ctx.Response.StatusCode); + ctx.Response.Body.Position = 0; + using var reader = new StreamReader(ctx.Response.Body); + var body = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); + Assert.Equal("endpoint", body); + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_use_global_mappers_as_fallback() + { + var actor = Sys.ActorOf(Props.Create()); + var globalMappers = new EntityResponseMapperCollection(); + + globalMappers.Add(async (ctx, resp) => + { + ctx.Response.StatusCode = 200; + await ctx.Response.WriteAsync(resp.Result); + }); + + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("hello")), + IsTell: false, + TimeoutOverride: null, + EndpointMappers: null, + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, globalMappers, + TimeSpan.FromSeconds(5), new TestResolver(actor)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("hello")); + + Assert.Equal(200, ctx.Response.StatusCode); + ctx.Response.Body.Position = 0; + using var reader = new StreamReader(ctx.Response.Body); + var body = await reader.ReadToEndAsync(TestContext.Current.CancellationToken); + Assert.Equal("hello", body); + } + + [Fact(Timeout = 5000)] + public async Task Ask_should_return_504_when_actor_throws_exception() + { + var thrower = Sys.ActorOf(Props.Create(() => new ThrowingActor())); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("error")), + IsTell: false, + TimeoutOverride: TimeSpan.FromMilliseconds(100), + EndpointMappers: new EntityResponseMapperCollection(), + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), new TestResolver(thrower)); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("error")); + + Assert.Equal(504, ctx.Response.StatusCode); + } + + [Fact(Timeout = 5000)] + public async Task Tell_should_return_202_when_resolver_throws() + { + var failingResolver = new FailingResolver(); + var config = new EntityMethodConfig( + MessageFactory: (Func)(() => new TestMessage("error")), + IsTell: true, + TimeoutOverride: null, + EndpointMappers: null, + TellResponseHandler: null); + + var dispatcher = new EntityDispatcher(config, new EntityResponseMapperCollection(), + TimeSpan.FromSeconds(5), failingResolver); + + var ctx = CreateTestContext(); + await dispatcher.DispatchAsync(ctx, new TestMessage("error")); + + Assert.Equal(503, ctx.Response.StatusCode); + } + + private sealed class BlackholeActor : ReceiveActor + { + public BlackholeActor() + { + ReceiveAny(_ => { }); + } + } + + private sealed class ThrowingActor : ReceiveActor + { + public ThrowingActor() + { + ReceiveAny(_ => throw new InvalidOperationException("Test error")); + } + } + + private sealed class FailingResolver : IEntityActorResolver + { + public ValueTask ResolveAsync(IServiceProvider services, CancellationToken cancellationToken) + { + throw new InvalidOperationException("Resolution failed"); + } + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore.Tests/Servus.Akka.AspNetCore.Tests.csproj b/src/Servus.Akka.AspNetCore.Tests/Servus.Akka.AspNetCore.Tests.csproj new file mode 100644 index 000000000..17c75354a --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/Servus.Akka.AspNetCore.Tests.csproj @@ -0,0 +1,32 @@ + + + + Exe + true + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Servus.Akka.AspNetCore.Tests/xunit.runner.json b/src/Servus.Akka.AspNetCore.Tests/xunit.runner.json new file mode 100644 index 000000000..c2f842686 --- /dev/null +++ b/src/Servus.Akka.AspNetCore.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/src/Servus.Akka.AspNetCore/AkkaResults.cs b/src/Servus.Akka.AspNetCore/AkkaResults.cs new file mode 100644 index 000000000..4ab6b26c9 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/AkkaResults.cs @@ -0,0 +1,21 @@ +using Akka; +using Akka.Streams; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http; +using Servus.Akka.Sse; + +namespace Servus.Akka.AspNetCore; + +public static class AkkaResults +{ + public static IResult Stream(Source, NotUsed> source, IMaterializer materializer, + string contentType = "application/octet-stream") + { + return new AkkaStreamResult(source, materializer, contentType); + } + + public static IResult ServerSentEvent(Source source, IMaterializer materializer) + { + return new AkkaSseResult(source, materializer); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore/AkkaSseResult.cs b/src/Servus.Akka.AspNetCore/AkkaSseResult.cs new file mode 100644 index 000000000..5f87325c2 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/AkkaSseResult.cs @@ -0,0 +1,20 @@ +using Akka; +using Akka.Streams; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http; +using Servus.Akka.Sse; +using Servus.Akka.Streams.IO; + +namespace Servus.Akka.AspNetCore; + +internal sealed class AkkaSseResult(Source source, IMaterializer materializer) : IResult +{ + public async Task ExecuteAsync(HttpContext httpContext) + { + httpContext.Response.StatusCode = 200; + httpContext.Response.ContentType = "text/event-stream"; + await source + .Via(SseFormatterFlow.Instance) + .RunWith(StreamSink.To(httpContext.Response.Body), materializer); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore/AkkaStreamResult.cs b/src/Servus.Akka.AspNetCore/AkkaStreamResult.cs new file mode 100644 index 000000000..35c0da0fb --- /dev/null +++ b/src/Servus.Akka.AspNetCore/AkkaStreamResult.cs @@ -0,0 +1,20 @@ +using Akka; +using Akka.Streams; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http; +using Servus.Akka.Streams.IO; + +namespace Servus.Akka.AspNetCore; + +internal sealed class AkkaStreamResult( + Source, NotUsed> source, + IMaterializer materializer, + string contentType) : IResult +{ + public async Task ExecuteAsync(HttpContext httpContext) + { + httpContext.Response.StatusCode = 200; + httpContext.Response.ContentType = contentType; + await source.RunWith(StreamSink.To(httpContext.Response.Body), materializer); + } +} \ No newline at end of file diff --git a/src/Servus.Akka.AspNetCore/EntityAskBuilder.cs b/src/Servus.Akka.AspNetCore/EntityAskBuilder.cs new file mode 100644 index 000000000..3657f8336 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityAskBuilder.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore; + +public sealed class EntityAskBuilder +{ + internal EntityResponseMapperCollection Mappers { get; } = new(); + internal TimeSpan? TimeoutOverride { get; private set; } + + public EntityAskBuilder Handle(Func handler) + { + Mappers.Add(handler); + return this; + } + + public EntityAskBuilder WithTimeout(TimeSpan timeout) + { + TimeoutOverride = timeout; + return this; + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityBuilder.cs b/src/Servus.Akka.AspNetCore/EntityBuilder.cs new file mode 100644 index 000000000..4d6116be0 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityBuilder.cs @@ -0,0 +1,73 @@ +using Akka.Actor; +using Akka.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Servus.Akka.AspNetCore; + +public sealed class EntityBuilder +{ + private readonly Dictionary _methods = new(StringComparer.OrdinalIgnoreCase); + private readonly EntityResponseMapperCollection _responseMappers = new(); + private TimeSpan _timeout = TimeSpan.FromSeconds(5); + private IEntityActorResolver _resolver = new ServiceProviderActorResolver(_ => ActorRefs.Nobody); + + internal IReadOnlyDictionary Methods => _methods; + internal EntityResponseMapperCollection ResponseMappers => _responseMappers; + internal TimeSpan Timeout => _timeout; + internal IEntityActorResolver Resolver => _resolver; + + public EntityMethodBuilder OnGet(Delegate messageFactory) + => AddMethod("GET", messageFactory); + + public EntityMethodBuilder OnPost(Delegate messageFactory) + => AddMethod("POST", messageFactory); + + public EntityMethodBuilder OnPut(Delegate messageFactory) + => AddMethod("PUT", messageFactory); + + public EntityMethodBuilder OnDelete(Delegate messageFactory) + => AddMethod("DELETE", messageFactory); + + public EntityMethodBuilder OnPatch(Delegate messageFactory) + => AddMethod("PATCH", messageFactory); + + public EntityBuilder WithTimeout(TimeSpan timeout) + { + _timeout = timeout; + return this; + } + + public EntityBuilder UseResolver(IEntityActorResolver resolver) + { + _resolver = resolver; + return this; + } + + public EntityBuilder UseActorRef() + => UseActorRef(registry => registry.Get()); + + public EntityBuilder UseActorRef(Func factory) + => UseResolver(new ServiceProviderActorResolver( + sp => factory(sp.GetRequiredService()))); + + public EntityBuilder Response(Func mapper) + { + _responseMappers.Add(mapper); + return this; + } + + private EntityMethodBuilder AddMethod(string method, Delegate messageFactory) + { + var builder = new EntityMethodBuilder(messageFactory); + _methods[method] = builder; + return builder; + } + + internal sealed record ServiceProviderActorResolver( + Func Factory) : IEntityActorResolver + { + public ValueTask ResolveAsync(IServiceProvider services, CancellationToken cancellationToken) + => ValueTask.FromResult(Factory(services)); + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityDelegateComposer.cs b/src/Servus.Akka.AspNetCore/EntityDelegateComposer.cs new file mode 100644 index 000000000..54676ae99 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityDelegateComposer.cs @@ -0,0 +1,50 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore; + +internal static class EntityDelegateComposer +{ + private static readonly MethodInfo DispatchAsyncMethod = + typeof(EntityDispatcher).GetMethod("DispatchAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; + + internal static Delegate Compose(Delegate messageFactory, EntityDispatcher dispatcher) + { + var factoryMethod = messageFactory.Method; + var factoryParams = factoryMethod.GetParameters(); + + var userParamExprs = factoryParams + .Select(p => Expression.Parameter(p.ParameterType, p.Name)) + .ToArray(); + + var ctxParam = Expression.Parameter(typeof(HttpContext), "httpContext"); + + Expression factoryCall; + if (messageFactory.Target is not null) + { + factoryCall = Expression.Call( + Expression.Constant(messageFactory.Target), + factoryMethod, + userParamExprs); + } + else + { + factoryCall = Expression.Call(factoryMethod, userParamExprs); + } + + var messageExpr = factoryMethod.ReturnType == typeof(object) + ? factoryCall + : Expression.Convert(factoryCall, typeof(object)); + + var dispatchCall = Expression.Call( + Expression.Constant(dispatcher), + DispatchAsyncMethod, + ctxParam, + messageExpr); + + var allParams = userParamExprs.Append(ctxParam).ToArray(); + var lambda = Expression.Lambda(dispatchCall, allParams); + return lambda.Compile(); + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityDispatcher.cs b/src/Servus.Akka.AspNetCore/EntityDispatcher.cs new file mode 100644 index 000000000..6a1a09c64 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityDispatcher.cs @@ -0,0 +1,77 @@ +using Akka.Actor; +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore; + +internal sealed class EntityDispatcher( + EntityMethodConfig methodConfig, + EntityResponseMapperCollection responseMappers, + TimeSpan timeout, + IEntityActorResolver resolver) +{ + internal async Task DispatchAsync(HttpContext ctx, object message) + { + if (methodConfig.IsTell) + { + await ExecuteTell(ctx, message); + } + else + { + await ExecuteAsk(ctx, message); + } + } + + private async Task ExecuteAsk(HttpContext ctx, object message) + { + try + { + var askTimeout = methodConfig.TimeoutOverride ?? timeout; + var actorRef = await resolver.ResolveAsync(ctx.RequestServices, ctx.RequestAborted); + var response = await actorRef.Ask(message, askTimeout, ctx.RequestAborted); + + var mapper = methodConfig.EndpointMappers?.FindMapper(response.GetType()) + ?? responseMappers.FindMapper(response.GetType()); + if (mapper is null) + { + ctx.Response.StatusCode = 500; + return; + } + + await mapper(ctx, response); + } + catch (TaskCanceledException) + { + ctx.Response.StatusCode = 504; + } + catch (AskTimeoutException) + { + ctx.Response.StatusCode = 504; + } + catch + { + ctx.Response.StatusCode = 500; + } + } + + private async Task ExecuteTell(HttpContext ctx, object message) + { + try + { + var actorRef = await resolver.ResolveAsync(ctx.RequestServices, ctx.RequestAborted); + actorRef.Tell(message); + + if (methodConfig.TellResponseHandler is not null) + { + await methodConfig.TellResponseHandler(ctx); + } + else + { + ctx.Response.StatusCode = 202; + } + } + catch + { + ctx.Response.StatusCode = 503; + } + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityEndpointExtensions.cs b/src/Servus.Akka.AspNetCore/EntityEndpointExtensions.cs new file mode 100644 index 000000000..235d45efa --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityEndpointExtensions.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Servus.Akka.AspNetCore; + +public static class EntityEndpointExtensions +{ + public static RouteHandlerBuilder MapEntity( + this IEndpointRouteBuilder builder, + string pattern, + Action configure) + { + var entityBuilder = new EntityBuilder(); + configure(entityBuilder); + return RegisterEndpoints(builder, pattern, entityBuilder); + } + + public static RouteHandlerBuilder MapEntity( + this IEndpointRouteBuilder builder, + string pattern, + Action configure) + { + var entityBuilder = new EntityBuilder(); + entityBuilder.UseActorRef(); + configure(entityBuilder); + return RegisterEndpoints(builder, pattern, entityBuilder); + } + + private static RouteHandlerBuilder RegisterEndpoints( + IEndpointRouteBuilder builder, + string pattern, + EntityBuilder entityBuilder) + { + RouteHandlerBuilder? lastBuilder = null; + + foreach (var (method, methodBuilder) in entityBuilder.Methods) + { + var config = methodBuilder.ToConfig(); + var dispatcher = new EntityDispatcher( + config, + entityBuilder.ResponseMappers, + entityBuilder.Timeout, + entityBuilder.Resolver); + + var compositeDelegate = EntityDelegateComposer.Compose( + config.MessageFactory, dispatcher); + + lastBuilder = builder.MapMethods(pattern, [method], compositeDelegate); + } + + if (lastBuilder is null) + { + throw new InvalidOperationException( + "MapEntity requires at least one HTTP method (OnGet, OnPost, etc.)."); + } + + return lastBuilder; + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityMethodBuilder.cs b/src/Servus.Akka.AspNetCore/EntityMethodBuilder.cs new file mode 100644 index 000000000..24644ef66 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityMethodBuilder.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore; + +public sealed class EntityMethodBuilder +{ + private readonly Delegate _messageFactory; + private bool _isTell; + private TimeSpan? _timeoutOverride; + private EntityResponseMapperCollection? _endpointMappers; + private Func? _tellResponseHandler; + + internal EntityMethodBuilder(Delegate messageFactory) + { + _messageFactory = messageFactory; + } + + public EntityMethodBuilder Ask(Action configure) + { + _isTell = false; + var builder = new EntityAskBuilder(); + configure(builder); + _endpointMappers = builder.Mappers; + _timeoutOverride = builder.TimeoutOverride ?? _timeoutOverride; + return this; + } + + public EntityMethodBuilder Tell(Action? configure = null) + { + _isTell = true; + if (configure is not null) + { + var builder = new EntityTellBuilder(); + configure(builder); + _tellResponseHandler = builder.ResponseHandler; + } + return this; + } + + public EntityMethodBuilder WithTimeout(TimeSpan timeout) + { + _timeoutOverride = timeout; + return this; + } + + internal EntityMethodConfig ToConfig() + { + return new EntityMethodConfig( + _messageFactory, + _isTell, + _timeoutOverride, + _endpointMappers, + _tellResponseHandler); + } +} diff --git a/src/Servus.Akka.AspNetCore/EntityMethodConfig.cs b/src/Servus.Akka.AspNetCore/EntityMethodConfig.cs new file mode 100644 index 000000000..1fdbdfa77 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/EntityMethodConfig.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; + +namespace Servus.Akka.AspNetCore; + +internal sealed record EntityMethodConfig( + Delegate MessageFactory, + bool IsTell, + TimeSpan? TimeoutOverride, + EntityResponseMapperCollection? EndpointMappers, + Func? TellResponseHandler); diff --git a/src/TurboHTTP/Routing/EntityResponseMapperCollection.cs b/src/Servus.Akka.AspNetCore/EntityResponseMapperCollection.cs similarity index 65% rename from src/TurboHTTP/Routing/EntityResponseMapperCollection.cs rename to src/Servus.Akka.AspNetCore/EntityResponseMapperCollection.cs index 1f5570394..aa8270fa8 100644 --- a/src/TurboHTTP/Routing/EntityResponseMapperCollection.cs +++ b/src/Servus.Akka.AspNetCore/EntityResponseMapperCollection.cs @@ -1,19 +1,19 @@ -using TurboHTTP.Server; +using Microsoft.AspNetCore.Http; -namespace TurboHTTP.Routing; +namespace Servus.Akka.AspNetCore; internal sealed class EntityResponseMapperCollection { - private readonly List<(Type Type, Func Mapper)> _mappers = []; + private readonly List<(Type Type, Func Mapper)> _mappers = []; internal int Count => _mappers.Count; - public void Add(Func mapper) + public void Add(Func mapper) { _mappers.Add((typeof(T), (ctx, obj) => mapper(ctx, (T)obj))); } - public Func? FindMapper(Type responseType) + public Func? FindMapper(Type responseType) { foreach (var (type, mapper) in _mappers) { diff --git a/src/TurboHTTP/Server/TurboEntityTellBuilder.cs b/src/Servus.Akka.AspNetCore/EntityTellBuilder.cs similarity index 52% rename from src/TurboHTTP/Server/TurboEntityTellBuilder.cs rename to src/Servus.Akka.AspNetCore/EntityTellBuilder.cs index 5437f76ca..9895c20ef 100644 --- a/src/TurboHTTP/Server/TurboEntityTellBuilder.cs +++ b/src/Servus.Akka.AspNetCore/EntityTellBuilder.cs @@ -1,11 +1,11 @@ using System.Net; using Microsoft.AspNetCore.Http; -namespace TurboHTTP.Server; +namespace Servus.Akka.AspNetCore; -public sealed class TurboEntityTellBuilder +public sealed class EntityTellBuilder { - internal Func? ResponseHandler { get; private set; } + internal Func? ResponseHandler { get; private set; } public void Produces(HttpStatusCode statusCode) { @@ -25,9 +25,8 @@ public void Produces(int statusCode) }; } - public void Handle(Func writer) - => ResponseHandler = async ctx => await writer(ctx); - - public void Produces(Func factory) - => ResponseHandler = async ctx => await factory(ctx).ExecuteAsync(ctx); -} \ No newline at end of file + public void Handle(Func writer) + { + ResponseHandler = writer; + } +} diff --git a/src/TurboHTTP/Routing/IEntityActorResolver.cs b/src/Servus.Akka.AspNetCore/IEntityActorResolver.cs similarity index 81% rename from src/TurboHTTP/Routing/IEntityActorResolver.cs rename to src/Servus.Akka.AspNetCore/IEntityActorResolver.cs index b7f45b042..97682eaaf 100644 --- a/src/TurboHTTP/Routing/IEntityActorResolver.cs +++ b/src/Servus.Akka.AspNetCore/IEntityActorResolver.cs @@ -1,8 +1,8 @@ using Akka.Actor; -namespace TurboHTTP.Routing; +namespace Servus.Akka.AspNetCore; public interface IEntityActorResolver { ValueTask ResolveAsync(IServiceProvider services, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/src/Servus.Akka.AspNetCore/Servus.Akka.AspNetCore.csproj b/src/Servus.Akka.AspNetCore/Servus.Akka.AspNetCore.csproj new file mode 100644 index 000000000..7991998c7 --- /dev/null +++ b/src/Servus.Akka.AspNetCore/Servus.Akka.AspNetCore.csproj @@ -0,0 +1,25 @@ + + + + false + + CA1416 + + + + + + + + + + + + + + + + + + + diff --git a/src/Servus.Akka.Tests/Transport/Quic/Client/QuicTransportStateMachineSpec.cs b/src/Servus.Akka.Tests/Transport/Quic/Client/QuicTransportStateMachineSpec.cs index 38856e95e..6ed40de03 100644 --- a/src/Servus.Akka.Tests/Transport/Quic/Client/QuicTransportStateMachineSpec.cs +++ b/src/Servus.Akka.Tests/Transport/Quic/Client/QuicTransportStateMachineSpec.cs @@ -439,7 +439,7 @@ public void Dispatch_InboundStreamAccepted_should_register_server_stream() { var (ops, sm) = CreateConnectedStateMachine(); - var streamId = 456L; + var streamId = 3L; var stream = new MemoryStream(); sm.Dispatch(new InboundStreamAccepted(stream, streamId)); diff --git a/src/Servus.Akka.Tests/Transport/Quic/QuicStreamStateSpec.cs b/src/Servus.Akka.Tests/Transport/Quic/QuicStreamStateSpec.cs index 078f309df..15f9bf76c 100644 --- a/src/Servus.Akka.Tests/Transport/Quic/QuicStreamStateSpec.cs +++ b/src/Servus.Akka.Tests/Transport/Quic/QuicStreamStateSpec.cs @@ -111,6 +111,20 @@ public void OnReadCompleted_in_Active_should_transition_to_HalfClosedRead() Assert.Equal(StreamPhase.HalfClosedRead, state.Phase); } + [Fact(Timeout = 5000)] + public void CompleteWrites_in_HalfClosedRead_should_transition_to_Closed() + { + var state = new QuicStreamState(StreamDirection.Bidirectional); + state.AttachHandle(new StreamHandle(new MemoryStream())); + state.OnReadCompleted(); + + Assert.Equal(StreamPhase.HalfClosedRead, state.Phase); + + state.CompleteWrites(); + + Assert.Equal(StreamPhase.Closed, state.Phase); + } + [Fact(Timeout = 5000)] public void Abort_should_transition_to_Closed() { diff --git a/src/Servus.Akka.Tests/Transport/TcpListenerOptionsSpec.cs b/src/Servus.Akka.Tests/Transport/TcpListenerOptionsSpec.cs index 07be6570b..badc6281d 100644 --- a/src/Servus.Akka.Tests/Transport/TcpListenerOptionsSpec.cs +++ b/src/Servus.Akka.Tests/Transport/TcpListenerOptionsSpec.cs @@ -1,4 +1,3 @@ -using System.Net.Security; using Servus.Akka.Transport; namespace Servus.Akka.Tests.Transport; diff --git a/src/Servus.Akka/Servus.Akka.csproj b/src/Servus.Akka/Servus.Akka.csproj index 9c601593c..d96d43822 100644 --- a/src/Servus.Akka/Servus.Akka.csproj +++ b/src/Servus.Akka/Servus.Akka.csproj @@ -1,9 +1,6 @@ - net10.0 - enable - enable false CA1416 diff --git a/src/TurboHTTP/Features/Sse/ServerSentEvent.cs b/src/Servus.Akka/Sse/ServerSentEvent.cs similarity index 80% rename from src/TurboHTTP/Features/Sse/ServerSentEvent.cs rename to src/Servus.Akka/Sse/ServerSentEvent.cs index adda3bcb1..ec8eeda4b 100644 --- a/src/TurboHTTP/Features/Sse/ServerSentEvent.cs +++ b/src/Servus.Akka/Sse/ServerSentEvent.cs @@ -1,4 +1,4 @@ -namespace TurboHTTP.Features.Sse; +namespace Servus.Akka.Sse; public sealed record ServerSentEvent( string Data, diff --git a/src/TurboHTTP/Features/Sse/SseFormatterFlow.cs b/src/Servus.Akka/Sse/SseFormatterFlow.cs similarity index 97% rename from src/TurboHTTP/Features/Sse/SseFormatterFlow.cs rename to src/Servus.Akka/Sse/SseFormatterFlow.cs index d27400314..936b0df82 100644 --- a/src/TurboHTTP/Features/Sse/SseFormatterFlow.cs +++ b/src/Servus.Akka/Sse/SseFormatterFlow.cs @@ -3,9 +3,9 @@ using Akka; using Akka.Streams.Dsl; -namespace TurboHTTP.Features.Sse; +namespace Servus.Akka.Sse; -internal static class SseFormatterFlow +public static class SseFormatterFlow { private const byte Lf = (byte)'\n'; diff --git a/src/TurboHTTP/Features/Sse/SseParserFlow.cs b/src/Servus.Akka/Sse/SseParserFlow.cs similarity index 84% rename from src/TurboHTTP/Features/Sse/SseParserFlow.cs rename to src/Servus.Akka/Sse/SseParserFlow.cs index d717cc5fe..b26398302 100644 --- a/src/TurboHTTP/Features/Sse/SseParserFlow.cs +++ b/src/Servus.Akka/Sse/SseParserFlow.cs @@ -4,21 +4,14 @@ using Akka.Streams.Dsl; using Akka.Streams.Stage; -namespace TurboHTTP.Features.Sse; +namespace Servus.Akka.Sse; -/// -/// Exposes the SSE parser as a reusable Flow. -/// -internal static class SseParserFlow +public static class SseParserFlow { public static Flow, ServerSentEvent, NotUsed> Instance { get; } = Flow.FromGraph(new SseParserStage()); } -/// -/// Stateful GraphStage that transforms raw byte chunks into parsed SSE events. -/// Handles multi-line data, comments, BOM stripping, CRLF/LF/CR line endings, and split chunks. -/// internal sealed class SseParserStage : GraphStage, ServerSentEvent>> { private readonly Inlet> _in = new("SseParserStage.in"); @@ -54,7 +47,6 @@ public SseParserLogic(SseParserStage stage) : base(stage.Shape) var chunk = Grab(stage._in); var bytes = chunk.ToArray(); - // Strip BOM if at start of stream (check bytes before decoding) var startIndex = 0; if (!_bomChecked) { @@ -73,14 +65,12 @@ public SseParserLogic(SseParserStage stage) : base(stage.Shape) { _upstreamFinished = true; - // Process any remaining buffered line as a field if (_lineBuffer.Length > 0) { ProcessField(_lineBuffer.ToString()); _lineBuffer.Clear(); } - // Emit pending event if has data if (_hasData) { var data = _dataAccumulator.ToString(); @@ -109,36 +99,29 @@ public SseParserLogic(SseParserStage stage) : base(stage.Shape) public override void PreStart() { - // Pull the first element from upstream to start the stream Pull(_stage._in); _upstreamWaiting = true; } private void DrainPending(SseParserStage stage) { - // Keep pushing while downstream is ready and we have events while (IsAvailable(stage._out) && _pending.Count > 0) { var evt = _pending.Dequeue(); Push(stage._out, evt); } - // After draining, check if we should pull more or complete if (!IsAvailable(stage._out)) { - // Downstream is not ready, we'll wait for next pull return; } - // Downstream is ready. Check if we should pull more or complete if (_upstreamFinished && _pending.Count == 0) { - // Upstream finished and no pending events - complete CompleteStage(); } else if (!_upstreamWaiting && !_upstreamFinished) { - // Pull more from upstream Pull(stage._in); _upstreamWaiting = true; } @@ -149,11 +132,9 @@ private void ProcessText(string text) var i = 0; while (i < text.Length) { - // Find next line ending var lineEnd = -1; var endLength = 0; - // Search for next line ending from position i for (var j = i; j < text.Length; j++) { if (j < text.Length - 1 && text[j] == '\r' && text[j + 1] == '\n') @@ -173,16 +154,13 @@ private void ProcessText(string text) if (lineEnd >= 0) { - // Found a line ending var lineContent = text.Substring(i, lineEnd - i); _lineBuffer.Append(lineContent); var completeLine = _lineBuffer.ToString(); _lineBuffer.Clear(); - // Process the complete line if (completeLine == string.Empty) { - // Empty line = event boundary if (_hasData) { var data = _dataAccumulator.ToString(); @@ -199,12 +177,10 @@ private void ProcessText(string text) _pending.Enqueue(evt); } - // Reset for next event ResetEvent(); } else if (!completeLine.StartsWith(":")) { - // Not a comment ProcessField(completeLine); } @@ -212,7 +188,6 @@ private void ProcessText(string text) } else { - // No line ending found - buffer remaining text var remaining = text[i..]; _lineBuffer.Append(remaining); break; @@ -236,7 +211,6 @@ private void ProcessField(string line) fieldName = line[..colonIndex]; var valueStart = colonIndex + 1; - // Strip leading space after colon if (valueStart < line.Length && line[valueStart] == ' ') { valueStart++; diff --git a/src/Servus.Akka/Transport/Quic/Client/QuicTransportStateMachine.cs b/src/Servus.Akka/Transport/Quic/Client/QuicTransportStateMachine.cs index b1091fd15..181d70bbb 100644 --- a/src/Servus.Akka/Transport/Quic/Client/QuicTransportStateMachine.cs +++ b/src/Servus.Akka/Transport/Quic/Client/QuicTransportStateMachine.cs @@ -223,6 +223,11 @@ private void HandleCompleteWrites(StreamTarget streamId) if (_streams.TryGetValue(streamId, out var state)) { state.CompleteWrites(); + if (state.Phase == StreamPhase.Closed) + { + _streams.Remove(streamId); + _ = state.DisposeAsync(); + } } _ops.OnSignalPullOutbound(); @@ -288,12 +293,15 @@ private void OnInboundStreamAccepted(Stream stream, long rawStreamId) { var streamId = StreamTarget.FromId(rawStreamId); var handle = new StreamHandle(stream); - var state = new QuicStreamState(StreamDirection.Unidirectional); + var direction = (rawStreamId & 0x02) != 0 + ? StreamDirection.Unidirectional + : StreamDirection.Bidirectional; + var state = new QuicStreamState(direction); state.AttachHandle(handle); _streams[streamId] = state; _pumpManager?.StartInboundPump(handle, rawStreamId, _connectionGen); - _ops.OnPushInbound(new ServerStreamAccepted(streamId, StreamDirection.Unidirectional)); + _ops.OnPushInbound(new ServerStreamAccepted(streamId, direction)); } private void OnInboundComplete(DisconnectReason reason, long rawStreamId) diff --git a/src/Servus.Akka/Transport/Quic/Listener/QuicServerStateMachine.cs b/src/Servus.Akka/Transport/Quic/Listener/QuicServerStateMachine.cs index fcac35b27..746f480c5 100644 --- a/src/Servus.Akka/Transport/Quic/Listener/QuicServerStateMachine.cs +++ b/src/Servus.Akka/Transport/Quic/Listener/QuicServerStateMachine.cs @@ -168,6 +168,11 @@ private void HandleCompleteWrites(StreamTarget streamId) if (_streams.TryGetValue(streamId, out var state)) { state.CompleteWrites(); + if (state.Phase == StreamPhase.Closed) + { + _streams.Remove(streamId); + _ = state.DisposeAsync(); + } } _ops.OnSignalPullOutbound(); @@ -195,7 +200,11 @@ private void OnStreamLeaseAcquired(StreamHandle handle, long rawStreamId) } state.AttachHandle(handle); - _pumpManager?.StartInboundPump(handle, rawStreamId, _connectionGen); + if (state.Direction == StreamDirection.Bidirectional) + { + _pumpManager?.StartInboundPump(handle, rawStreamId, _connectionGen); + } + _ops.OnPushInbound(new StreamOpened(streamId, state.Direction)); } @@ -203,12 +212,15 @@ private void OnInboundStreamAccepted(Stream stream, long rawStreamId) { var streamId = StreamTarget.FromId(rawStreamId); var handle = new StreamHandle(stream); - var state = new QuicStreamState(StreamDirection.Unidirectional); + var direction = (rawStreamId & 0x02) != 0 + ? StreamDirection.Unidirectional + : StreamDirection.Bidirectional; + var state = new QuicStreamState(direction); state.AttachHandle(handle); _streams[streamId] = state; _pumpManager?.StartInboundPump(handle, rawStreamId, _connectionGen); - _ops.OnPushInbound(new ServerStreamAccepted(streamId, StreamDirection.Unidirectional)); + _ops.OnPushInbound(new ServerStreamAccepted(streamId, direction)); } private void OnInboundComplete(DisconnectReason reason, long rawStreamId) diff --git a/src/Servus.Akka/Transport/Quic/QuicStreamState.cs b/src/Servus.Akka/Transport/Quic/QuicStreamState.cs index 7d3232b12..1ad79cae5 100644 --- a/src/Servus.Akka/Transport/Quic/QuicStreamState.cs +++ b/src/Servus.Akka/Transport/Quic/QuicStreamState.cs @@ -74,6 +74,10 @@ public void CompleteWrites() _handle?.CompleteWrites(); Phase = StreamPhase.HalfClosedWrite; return; + case StreamPhase.HalfClosedRead: + _handle?.CompleteWrites(); + Phase = StreamPhase.Closed; + return; } } diff --git a/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index fc4113a24..0d7629ce8 100644 --- a/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/TurboHTTP.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -4,7 +4,6 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.IntegrationTests.Client")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.IntegrationTests.End2End")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.IntegrationTests.Server")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.MicroBenchmarks")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TurboHTTP.Tests.Shared")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] @@ -30,7 +29,7 @@ namespace TurboHTTP.Client } public static class Extensions { - public static Akka.Streams.Dsl.Source AsEventStream(this System.Net.Http.HttpResponseMessage response) { } + public static Akka.Streams.Dsl.Source AsEventStream(this System.Net.Http.HttpResponseMessage response) { } public static System.Threading.Tasks.ValueTask GetResponseAsync(this System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken ct = default) { } } public sealed class Http1Options @@ -188,186 +187,6 @@ namespace TurboHTTP.Client public System.TimeSpan Timeout { get; init; } } } -namespace TurboHTTP.Context.Features -{ - public interface ITlsHandshakeFeature - { - string? HostName { get; } - System.Net.Security.SslApplicationProtocol NegotiatedApplicationProtocol { get; } - System.Net.Security.TlsCipherSuite? NegotiatedCipherSuite { get; } - System.Security.Authentication.SslProtocols Protocol { get; } - } - public interface ITurboConnectionFeature - { - string ConnectionId { get; set; } - System.Net.IPAddress? LocalIpAddress { get; set; } - int LocalPort { get; set; } - System.Net.IPAddress? RemoteIpAddress { get; set; } - int RemotePort { get; set; } - } - public interface ITurboFeatureCollection - { - T? Get() - where T : class; - void Set(T? feature) - where T : class; - } - public interface ITurboRequestBodyDetectionFeature - { - bool CanHaveBody { get; } - } - public interface ITurboRequestBodyFeature - { - System.IO.Stream Body { get; } - Akka.Streams.Dsl.Source, Akka.NotUsed> BodySource { get; } - } - public interface ITurboRequestFeature - { - System.IO.Stream Body { get; set; } - TurboHTTP.Context.ITurboHeaderDictionary Headers { get; } - string Method { get; set; } - string Path { get; set; } - string PathBase { get; set; } - string Protocol { get; set; } - string QueryString { get; set; } - string RawTarget { get; set; } - string Scheme { get; set; } - } - public interface ITurboRequestIdentifierFeature - { - string TraceIdentifier { get; set; } - } - public interface ITurboRequestLifetimeFeature - { - System.Threading.CancellationToken RequestAborted { get; set; } - void Abort(); - } - public interface ITurboResetFeature - { - void Reset(int errorCode); - } - public interface ITurboResponseBodyFeature : Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature - { - Akka.Streams.Dsl.Sink, System.Threading.Tasks.Task> BodySink { get; } - System.Threading.Tasks.Task WhenSinkCompleted { get; } - } - public interface ITurboResponseFeature - { - System.IO.Stream Body { get; set; } - bool HasStarted { get; } - TurboHTTP.Context.ITurboHeaderDictionary Headers { get; } - string? ReasonPhrase { get; set; } - int StatusCode { get; set; } - void OnCompleted(System.Func callback, object? state); - void OnStarting(System.Func callback, object? state); - } - public interface ITurboResponseTrailersFeature - { - TurboHTTP.Context.ITurboHeaderDictionary Trailers { get; set; } - } -} -namespace TurboHTTP.Context -{ - public interface ITurboFormCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable - { - int Count { get; } - TurboHTTP.Context.ITurboFormFileCollection Files { get; } - Microsoft.Extensions.Primitives.StringValues this[string key] { get; } - System.Collections.Generic.ICollection Keys { get; } - bool ContainsKey(string key); - } - public interface ITurboFormFile - { - string ContentType { get; } - string FileName { get; } - long Length { get; } - string Name { get; } - void CopyTo(System.IO.Stream target); - System.Threading.Tasks.Task CopyToAsync(System.IO.Stream target, System.Threading.CancellationToken cancellationToken = default); - System.IO.Stream OpenReadStream(); - } - public interface ITurboFormFileCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable - { - int Count { get; } - TurboHTTP.Context.ITurboFormFile this[int index] { get; } - TurboHTTP.Context.ITurboFormFile? this[string name] { get; } - TurboHTTP.Context.ITurboFormFile? GetFile(string name); - System.Collections.Generic.IReadOnlyList GetFiles(string name); - } - public interface ITurboHeaderDictionary : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable - { - long? ContentLength { get; set; } - int Count { get; } - Microsoft.Extensions.Primitives.StringValues this[string key] { get; set; } - System.Collections.Generic.ICollection Keys { get; } - void Add(string key, Microsoft.Extensions.Primitives.StringValues value); - void Clear(); - bool ContainsKey(string key); - bool Remove(string key); - bool TryGetValue(string key, out Microsoft.Extensions.Primitives.StringValues value); - } - public interface ITurboQueryCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable - { - int Count { get; } - Microsoft.Extensions.Primitives.StringValues this[string key] { get; } - System.Collections.Generic.ICollection Keys { get; } - bool ContainsKey(string key); - bool TryGetValue(string key, out Microsoft.Extensions.Primitives.StringValues value); - } - public interface ITurboRequestCookieCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable - { - int Count { get; } - string? this[string key] { get; } - System.Collections.Generic.ICollection Keys { get; } - bool ContainsKey(string key); - } - public sealed class TurboHttpRequest : Microsoft.AspNetCore.Http.HttpRequest - { - public TurboHttpRequest(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { } - public override System.IO.Stream Body { get; set; } - public override System.IO.Pipelines.PipeReader BodyReader { get; } - public Akka.Streams.Dsl.Source, Akka.NotUsed> BodySource { get; } - public System.Net.Http.HttpContent? Content { get; } - public override long? ContentLength { get; set; } - public override string? ContentType { get; set; } - public override Microsoft.AspNetCore.Http.IRequestCookieCollection Cookies { get; set; } - public override Microsoft.AspNetCore.Http.IFormCollection Form { get; set; } - public override bool HasFormContentType { get; } - public override Microsoft.AspNetCore.Http.IHeaderDictionary Headers { get; } - public override Microsoft.AspNetCore.Http.HostString Host { get; set; } - public override Microsoft.AspNetCore.Http.HttpContext HttpContext { get; } - public override bool IsHttps { get; set; } - public override string Method { get; set; } - public override Microsoft.AspNetCore.Http.PathString Path { get; set; } - public override Microsoft.AspNetCore.Http.PathString PathBase { get; set; } - public override string Protocol { get; set; } - public override Microsoft.AspNetCore.Http.IQueryCollection Query { get; set; } - public override Microsoft.AspNetCore.Http.QueryString QueryString { get; set; } - public System.Uri? RequestUri { get; } - public override Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { get; set; } - public override string Scheme { get; set; } - public override System.Threading.Tasks.Task ReadFormAsync(System.Threading.CancellationToken cancellationToken = default) { } - } - public sealed class TurboHttpResponse : Microsoft.AspNetCore.Http.HttpResponse - { - public TurboHttpResponse(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { } - public override System.IO.Stream Body { get; set; } - public override System.IO.Pipelines.PipeWriter BodyWriter { get; } - public override long? ContentLength { get; set; } - public override string? ContentType { get; set; } - public override Microsoft.AspNetCore.Http.IResponseCookies Cookies { get; } - public override bool HasStarted { get; } - public override Microsoft.AspNetCore.Http.IHeaderDictionary Headers { get; } - public override Microsoft.AspNetCore.Http.HttpContext HttpContext { get; } - public override int StatusCode { get; set; } - public void AppendTrailer(string name, string value) { } - public void DeclareTrailer(string name) { } - public Microsoft.AspNetCore.Http.IHeaderDictionary GetTrailers() { } - public override void OnCompleted(System.Func callback, object state) { } - public override void OnStarting(System.Func callback, object state) { } - public override void Redirect(string location, bool permanent = false) { } - } -} namespace TurboHTTP.Diagnostics { public static class TurboTraceExtensions @@ -375,6 +194,8 @@ namespace TurboHTTP.Diagnostics public static OpenTelemetry.Metrics.MeterProviderBuilder AddTurboHttpInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) { } public static OpenTelemetry.Trace.TracerProviderBuilder AddTurboHttpInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) { } public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboLoggerTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Servus.Core.Diagnostics.TraceLevel minimumLevel = 1, System.Func? categoryFilter = null) { } + public static OpenTelemetry.Metrics.MeterProviderBuilder AddTurboServerInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) { } + public static OpenTelemetry.Trace.TracerProviderBuilder AddTurboServerInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) { } public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Servus.Core.Diagnostics.IServusTraceListener listener, Servus.Core.Diagnostics.TraceLevel minimumLevel = 1, System.Func? categoryFilter = null) { } } } @@ -500,85 +321,70 @@ namespace TurboHTTP.Features.Cookies None = 3, } } -namespace TurboHTTP.Features.Sse +namespace TurboHTTP.Server.Context.Features { - public sealed class ServerSentEvent : System.IEquatable + public interface ITlsHandshakeFeature { - public ServerSentEvent(string Data, string? EventType = null, string? Id = null, System.TimeSpan? Retry = default) { } - public string Data { get; init; } - public string? EventType { get; init; } - public string? Id { get; init; } - public System.TimeSpan? Retry { get; init; } + string? HostName { get; } + System.Net.Security.SslApplicationProtocol NegotiatedApplicationProtocol { get; } + System.Net.Security.TlsCipherSuite? NegotiatedCipherSuite { get; } + System.Security.Authentication.SslProtocols Protocol { get; } } } -namespace TurboHTTP.Routing +namespace TurboHTTP.Server.Context { - public sealed class AuthorizeData : System.IEquatable, TurboHTTP.Routing.IAuthorizeData - { - public AuthorizeData(string? Policy, string? Roles, string? AuthenticationSchemes) { } - public string? AuthenticationSchemes { get; init; } - public string? Policy { get; init; } - public string? Roles { get; init; } - } - public sealed class EndpointMetadata - { - public EndpointMetadata() { } - public bool AllowsAnonymous { get; } - public System.Collections.Generic.List AuthorizationPolicies { get; } - public string? DisplayName { get; } - public System.Collections.Generic.List Items { get; } - public string? Name { get; } - public bool RequiresAuthorization { get; } - public System.Collections.Generic.List Tags { get; } - } - public interface IAllowAnonymous { } - public interface IAuthorizeData - { - string? AuthenticationSchemes { get; } - string? Policy { get; } - string? Roles { get; } - } - public interface IEntityActorResolver - { - System.Threading.Tasks.ValueTask ResolveAsync(System.IServiceProvider services, System.Threading.CancellationToken cancellationToken); - } - public interface ITagsMetadata + public interface ITurboFormCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { - System.Collections.Generic.IReadOnlyList Tags { get; } + int Count { get; } + TurboHTTP.Server.Context.ITurboFormFileCollection Files { get; } + Microsoft.Extensions.Primitives.StringValues this[string key] { get; } + System.Collections.Generic.ICollection Keys { get; } + bool ContainsKey(string key); } - public sealed class RouteMatchResult + public interface ITurboFormFile { - public static readonly TurboHTTP.Routing.RouteMatchResult NoMatch; - public bool IsMatch { get; } - public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { get; } + string ContentType { get; } + string FileName { get; } + long Length { get; } + string Name { get; } + void CopyTo(System.IO.Stream target); + System.Threading.Tasks.Task CopyToAsync(System.IO.Stream target, System.Threading.CancellationToken cancellationToken = default); + System.IO.Stream OpenReadStream(); } - public sealed class RouteTable + public interface ITurboFormFileCollection : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable { - public TurboHTTP.Routing.RouteMatchResult Match(string method, string path) { } + int Count { get; } + TurboHTTP.Server.Context.ITurboFormFile this[int index] { get; } + TurboHTTP.Server.Context.ITurboFormFile? this[string name] { get; } + TurboHTTP.Server.Context.ITurboFormFile? GetFile(string name); + System.Collections.Generic.IReadOnlyList GetFiles(string name); } - public sealed class TagsMetadata : System.IEquatable, TurboHTTP.Routing.ITagsMetadata + public interface ITurboHeaderDictionary : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { - public TagsMetadata(System.Collections.Generic.IReadOnlyList Tags) { } - public System.Collections.Generic.IReadOnlyList Tags { get; init; } + long? ContentLength { get; set; } + int Count { get; } + Microsoft.Extensions.Primitives.StringValues this[string key] { get; set; } + System.Collections.Generic.ICollection Keys { get; } + void Add(string key, Microsoft.Extensions.Primitives.StringValues value); + void Clear(); + bool ContainsKey(string key); + bool Remove(string key); + bool TryGetValue(string key, out Microsoft.Extensions.Primitives.StringValues value); } - public sealed class TurboEndpointMetadata + public interface ITurboQueryCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { - public TurboEndpointMetadata(System.Collections.Generic.IReadOnlyList items) { } - public System.Collections.Generic.IReadOnlyList Items { get; } - public T? GetMetadata() - where T : class { } - public System.Collections.Generic.IEnumerable GetOrderedMetadata() - where T : class { } - public bool HasMetadata() - where T : class { } - public static TurboHTTP.Routing.TurboEndpointMetadata Merge(TurboHTTP.Routing.TurboEndpointMetadata group, TurboHTTP.Routing.TurboEndpointMetadata route) { } + int Count { get; } + Microsoft.Extensions.Primitives.StringValues this[string key] { get; } + System.Collections.Generic.ICollection Keys { get; } + bool ContainsKey(string key); + bool TryGetValue(string key, out Microsoft.Extensions.Primitives.StringValues value); } - public sealed class TurboRouteTable + public interface ITurboRequestCookieCollection : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { - public TurboRouteTable() { } - public TurboHTTP.Server.TurboRouteHandlerBuilder Add(string method, string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder Add(string method, string pattern, System.Func handler) { } - public TurboHTTP.Server.TurboRouteGroupBuilder CreateGroup(string prefix) { } + int Count { get; } + string? this[string key] { get; } + System.Collections.Generic.ICollection Keys { get; } + bool ContainsKey(string key); } } namespace TurboHTTP.Server @@ -638,26 +444,6 @@ namespace TurboHTTP.Server { public static System.Collections.Generic.List ToAlpnProtocols(this TurboHTTP.Server.HttpProtocols protocols) { } } - public interface ITurboApplicationBuilder - { - TurboHTTP.Server.ITurboApplicationBuilder Map(string pathPrefix, System.Action configure); - TurboHTTP.Server.ITurboApplicationBuilder MapWhen(System.Func predicate, System.Action configure); - TurboHTTP.Server.ITurboApplicationBuilder Run(TurboHTTP.Server.TurboRequestDelegate handler); - TurboHTTP.Server.ITurboApplicationBuilder Use(System.Func middleware); - TurboHTTP.Server.ITurboApplicationBuilder Use() - where T : class, TurboHTTP.Server.ITurboMiddleware; - } - public interface ITurboEndpointRouteBuilder - { - [System.Obsolete("Use extension methods to register routes. Direct RouteTable access will be remove" + - "d in 2.0.")] - TurboHTTP.Routing.TurboRouteTable RouteTable { get; } - System.IServiceProvider ServiceProvider { get; } - } - public interface ITurboMiddleware - { - System.Threading.Tasks.Task InvokeAsync(TurboHTTP.Server.TurboHttpContext context, TurboHTTP.Server.TurboRequestDelegate next); - } public sealed class ListenerBinding { public ListenerBinding() { } @@ -665,115 +451,6 @@ namespace TurboHTTP.Server public required Servus.Akka.Transport.IListenerFactory Factory { get; init; } public required Servus.Akka.Transport.ListenerOptions Options { get; init; } } - public sealed class ProducesMetadata : System.IEquatable - { - public ProducesMetadata(System.Type Type, int StatusCode) { } - public int StatusCode { get; init; } - public System.Type Type { get; init; } - } - public sealed class ProducesProblemMetadata : System.IEquatable - { - public ProducesProblemMetadata(int StatusCode) { } - public int StatusCode { get; init; } - } - public sealed class TurboConnectionInfo : Microsoft.AspNetCore.Http.ConnectionInfo - { - public TurboConnectionInfo(string id, System.Net.IPAddress? remoteIpAddress, int remotePort, System.Net.IPAddress? localIpAddress, int localPort) { } - public override System.Security.Cryptography.X509Certificates.X509Certificate2? ClientCertificate { get; set; } - public override string Id { get; set; } - public override System.Net.IPAddress? LocalIpAddress { get; set; } - public override int LocalPort { get; set; } - public override System.Net.IPAddress? RemoteIpAddress { get; set; } - public override int RemotePort { get; set; } - public override System.Threading.Tasks.Task GetClientCertificateAsync(System.Threading.CancellationToken cancellationToken = default) { } - } - public static class TurboEndpointRouteBuilderExtensions - { - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapDelete(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapDelete(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Func handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapEntity(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Action configure) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapEntity(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Action configure) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapGet(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapGet(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Func handler) { } - public static TurboHTTP.Server.TurboRouteGroupBuilder MapGroup(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string prefix) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapMethods(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Collections.Generic.IEnumerable methods, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapMethods(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Collections.Generic.IEnumerable methods, System.Func handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPatch(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPatch(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Func handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPost(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPost(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Func handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPut(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Delegate handler) { } - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapPut(this TurboHTTP.Server.ITurboEndpointRouteBuilder builder, string pattern, System.Func handler) { } - } - public sealed class TurboEntityAskBuilder - { - public TurboEntityAskBuilder() { } - public TurboHTTP.Server.TurboEntityAskBuilder Handle(System.Func handler) { } - public TurboHTTP.Server.TurboEntityAskBuilder Produces(System.Func handler) { } - public TurboHTTP.Server.TurboEntityAskBuilder WithTimeout(System.TimeSpan timeout) { } - } - public sealed class TurboEntityBuilder - { - public TurboEntityBuilder(string pattern) { } - public TurboHTTP.Server.TurboEntityMethodBuilder OnDelete(System.Delegate messageFactory) { } - public TurboHTTP.Server.TurboEntityMethodBuilder OnGet(System.Delegate messageFactory) { } - public TurboHTTP.Server.TurboEntityMethodBuilder OnPatch(System.Delegate messageFactory) { } - public TurboHTTP.Server.TurboEntityMethodBuilder OnPost(System.Delegate messageFactory) { } - public TurboHTTP.Server.TurboEntityMethodBuilder OnPut(System.Delegate messageFactory) { } - public TurboHTTP.Server.TurboEntityBuilder Response(System.Func mapper) { } - public TurboHTTP.Server.TurboEntityBuilder UseActorRef(System.Func actorRefFactory) { } - public TurboHTTP.Server.TurboEntityBuilder UseActorRef(System.Func factory) { } - public TurboHTTP.Server.TurboEntityBuilder UseActorRef() { } - public TurboHTTP.Server.TurboEntityBuilder UseResolver(TurboHTTP.Routing.IEntityActorResolver resolver) { } - public TurboHTTP.Server.TurboEntityBuilder UseResolver() - where TResolver : TurboHTTP.Routing.IEntityActorResolver, new () { } - public TurboHTTP.Server.TurboEntityBuilder WithTimeout(System.TimeSpan timeout) { } - } - public sealed class TurboEntityMethodBuilder - { - public void Ask(System.Action configure) { } - public void Tell(System.Action? configure = null) { } - public TurboHTTP.Server.TurboEntityMethodBuilder WithTimeout(System.TimeSpan timeout) { } - } - public sealed class TurboEntityTellBuilder - { - public TurboEntityTellBuilder() { } - public void Handle(System.Func writer) { } - public void Produces(System.Func factory) { } - public void Produces(System.Net.HttpStatusCode statusCode) { } - public void Produces(int statusCode) { } - } - public sealed class TurboHostBuilder - { - public TurboHTTP.Server.TurboHostBuilder ConfigureAppConfiguration(System.Action configure) { } - public TurboHTTP.Server.TurboHostBuilder ConfigureHostOptions(System.Action configure) { } - public TurboHTTP.Server.TurboHostBuilder ConfigureServices(System.Action configure) { } - } - public sealed class TurboHttpContext : Microsoft.AspNetCore.Http.HttpContext - { - public TurboHttpContext(Microsoft.AspNetCore.Http.Features.IFeatureCollection features, TurboHTTP.Server.TurboConnectionInfo connectionInfo, System.IServiceProvider? services, System.Threading.CancellationToken requestAborted, Akka.Streams.IMaterializer materializer) { } - public override Microsoft.AspNetCore.Http.ConnectionInfo Connection { get; } - public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } - public override System.Collections.Generic.IDictionary Items { get; set; } - public Akka.Streams.IMaterializer Materializer { get; set; } - public override Microsoft.AspNetCore.Http.HttpRequest Request { get; } - public override System.Threading.CancellationToken RequestAborted { get; set; } - public override System.IServiceProvider RequestServices { get; set; } - public override Microsoft.AspNetCore.Http.HttpResponse Response { get; } - public override Microsoft.AspNetCore.Http.ISession Session { get; set; } - public override string TraceIdentifier { get; set; } - public TurboHTTP.Context.TurboHttpRequest TurboRequest { get; } - public TurboHTTP.Context.TurboHttpResponse TurboResponse { get; } - public override System.Security.Claims.ClaimsPrincipal User { get; set; } - public override Microsoft.AspNetCore.Http.WebSocketManager WebSockets { get; } - public override void Abort() { } - } - public static class TurboHttpContextExtensions - { - public static TurboHTTP.Routing.TurboEndpointMetadata? GetEndpointMetadata(this TurboHTTP.Server.TurboHttpContext context) { } - public static bool HasEndpointMetadata(this TurboHTTP.Server.TurboHttpContext context) - where T : class { } - } public sealed class TurboHttpsOptions { public TurboHttpsOptions() { } @@ -801,72 +478,14 @@ namespace TurboHTTP.Server public void UseHttps(string path, string? password = null) { } public void UseHttps(string path, string? password, System.Action configure) { } } - public static class TurboMiddlewareExtensions - { - [System.Obsolete("Use TurboWebApplication with Map() instead. Will be removed in 2.0.")] - public static Microsoft.AspNetCore.Builder.WebApplication MapTurbo(this Microsoft.AspNetCore.Builder.WebApplication app, string pathPrefix, System.Action configure) { } - [System.Obsolete("Use TurboWebApplication with MapWhen() instead. Will be removed in 2.0.")] - public static Microsoft.AspNetCore.Builder.WebApplication MapTurboWhen(this Microsoft.AspNetCore.Builder.WebApplication app, System.Func predicate, System.Action configure) { } - [System.Obsolete("Use TurboWebApplication with Run() instead. Will be removed in 2.0.")] - public static Microsoft.AspNetCore.Builder.WebApplication RunTurbo(this Microsoft.AspNetCore.Builder.WebApplication app, TurboHTTP.Server.TurboRequestDelegate handler) { } - [System.Obsolete("Use TurboWebApplication with Use() instead. Will be removed in 2.0.")] - public static Microsoft.AspNetCore.Builder.WebApplication UseTurbo(this Microsoft.AspNetCore.Builder.WebApplication app, System.Func middleware) { } - [System.Obsolete("Use TurboWebApplication with Use() instead. Will be removed in 2.0.")] - public static Microsoft.AspNetCore.Builder.WebApplication UseTurbo(this Microsoft.AspNetCore.Builder.WebApplication app) - where T : class, TurboHTTP.Server.ITurboMiddleware { } - } - public delegate System.Threading.Tasks.Task TurboRequestDelegate(TurboHTTP.Server.TurboHttpContext context); - public sealed class TurboRouteGroupBuilder - { - public TurboHTTP.Server.TurboRouteGroupBuilder AllowAnonymous() { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapDelete(string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapEntity(string pattern, System.Action configure) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapEntity(string pattern, System.Action configure) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapGet(string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteGroupBuilder MapGroup(string prefix) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapMethods(string pattern, System.Collections.Generic.IEnumerable methods, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapPatch(string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapPost(string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder MapPut(string pattern, System.Delegate handler) { } - public TurboHTTP.Server.TurboRouteGroupBuilder RequireAuthorization() { } - public TurboHTTP.Server.TurboRouteGroupBuilder RequireAuthorization(string? policy) { } - public TurboHTTP.Server.TurboRouteGroupBuilder WithMetadata(params object[] metadata) { } - public TurboHTTP.Server.TurboRouteGroupBuilder WithTags(params string[] tags) { } - } - public sealed class TurboRouteHandlerBuilder - { - public TurboRouteHandlerBuilder() { } - public TurboHTTP.Routing.EndpointMetadata Metadata { get; } - public TurboHTTP.Server.TurboRouteHandlerBuilder AllowAnonymous() { } - public TurboHTTP.Server.TurboRouteHandlerBuilder Produces(int statusCode = 200) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder ProducesProblem(int statusCode = 500) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder RequireAuthorization() { } - public TurboHTTP.Server.TurboRouteHandlerBuilder RequireAuthorization(string? policy) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder WithDisplayName(string displayName) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder WithMetadata(params object[] metadata) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder WithName(string name) { } - public TurboHTTP.Server.TurboRouteHandlerBuilder WithTags(params string[] tags) { } - } - public static class TurboRoutingExtensions - { - [System.Obsolete("Use TurboWebApplication with MapDelete() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboDelete(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Delegate handler) { } - [System.Obsolete("Use TurboWebApplication with MapEntity() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboEntity(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Action configure) { } - [System.Obsolete("Use TurboWebApplication with MapEntity() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboEntity(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Action configure) { } - [System.Obsolete("Use TurboWebApplication with MapGet() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboGet(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Delegate handler) { } - [System.Obsolete("Use TurboWebApplication with MapGroup() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteGroupBuilder MapTurboGroup(this Microsoft.AspNetCore.Builder.WebApplication app, string prefix) { } - [System.Obsolete("Use TurboWebApplication with MapMethods() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboMethods(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Collections.Generic.IEnumerable methods, System.Delegate handler) { } - [System.Obsolete("Use TurboWebApplication with MapPatch() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboPatch(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Delegate handler) { } - [System.Obsolete("Use TurboWebApplication with MapPost() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboPost(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Delegate handler) { } - [System.Obsolete("Use TurboWebApplication with MapPut() instead. Will be removed in 2.0.")] - public static TurboHTTP.Server.TurboRouteHandlerBuilder MapTurboPut(this Microsoft.AspNetCore.Builder.WebApplication app, string pattern, System.Delegate handler) { } + public sealed class TurboServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable + { + public TurboServer(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.IServiceProvider services) { } + public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } + public void Dispose() { } + public System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application, System.Threading.CancellationToken cancellationToken) + where TContext : notnull { } + public System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { } } public sealed class TurboServerLimits { @@ -895,15 +514,7 @@ namespace TurboHTTP.Server public TurboHTTP.Server.Http1ServerOptions Http1 { get; } public TurboHTTP.Server.Http2ServerOptions Http2 { get; } public TurboHTTP.Server.Http3ServerOptions Http3 { get; } - [System.Obsolete("Use Limits.KeepAliveTimeout instead")] - public System.TimeSpan KeepAliveTimeout { get; set; } public TurboHTTP.Server.TurboServerLimits Limits { get; } - [System.Obsolete("Use Limits.MaxConcurrentConnections instead")] - public int MaxConcurrentConnections { get; set; } - [System.Obsolete("Use Limits.MaxConcurrentUpgradedConnections instead")] - public int MaxConcurrentUpgradedConnections { get; set; } - [System.Obsolete("Use Limits.RequestHeadersTimeout instead")] - public System.TimeSpan RequestHeadersTimeout { get; set; } public int ResponseBodyChunkSize { get; set; } public System.Collections.Generic.IList Urls { get; } public void Bind(Servus.Akka.Transport.QuicListenerOptions options) { } @@ -921,63 +532,8 @@ namespace TurboHTTP.Server public void ListenLocalhost(ushort port) { } public void ListenLocalhost(ushort port, System.Action configure) { } } - public static class TurboServerServiceCollectionExtensions - { - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboKestrel(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action? configure = null) { } - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddTurboKestrel(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action? configure = null) { } - } - public static class TurboStreamResults - { - public static Microsoft.AspNetCore.Http.IResult EventStream(Akka.Streams.Dsl.Source source) { } - public static Microsoft.AspNetCore.Http.IResult EventStream(Akka.Streams.Dsl.Source source) { } - public static Microsoft.AspNetCore.Http.IResult Stream(Akka.Streams.Dsl.Source, Akka.NotUsed> source, string? contentType = null) { } - } - public sealed class TurboWebApplication : Microsoft.Extensions.Hosting.IHost, System.IAsyncDisposable, System.IDisposable, TurboHTTP.Server.ITurboApplicationBuilder, TurboHTTP.Server.ITurboEndpointRouteBuilder + public static class TurboServerWebHostBuilderExtensions { - public Microsoft.Extensions.Configuration.IConfiguration Configuration { get; } - public Microsoft.Extensions.Hosting.IHostEnvironment Environment { get; } - public Microsoft.Extensions.Hosting.IHostApplicationLifetime Lifetime { get; } - public Microsoft.Extensions.Logging.ILogger Logger { get; } - public System.IServiceProvider Services { get; } - public System.Collections.Generic.ICollection Urls { get; } - public void Dispose() { } - public System.Threading.Tasks.ValueTask DisposeAsync() { } - public TurboHTTP.Server.ITurboApplicationBuilder Map(string pathPrefix, System.Action configure) { } - public TurboHTTP.Server.ITurboApplicationBuilder MapWhen(System.Func predicate, System.Action configure) { } - public TurboHTTP.Server.ITurboApplicationBuilder Run(TurboHTTP.Server.TurboRequestDelegate handler) { } - public System.Threading.Tasks.Task RunAsync(System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task RunAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken = default) { } - public TurboHTTP.Server.ITurboApplicationBuilder Use(System.Func middleware) { } - public TurboHTTP.Server.ITurboApplicationBuilder Use() - where T : class, TurboHTTP.Server.ITurboMiddleware { } - public System.Threading.Tasks.Task WaitForShutdownAsync(System.Threading.CancellationToken cancellationToken = default) { } - public static TurboHTTP.Server.TurboWebApplication Create(string[]? args = null) { } - public static TurboHTTP.Server.TurboWebApplicationBuilder CreateBuilder() { } - public static TurboHTTP.Server.TurboWebApplicationBuilder CreateBuilder(string[] args) { } - } - public sealed class TurboWebApplicationBuilder - { - public Microsoft.Extensions.Configuration.ConfigurationManager Configuration { get; } - public Microsoft.Extensions.Hosting.IHostEnvironment Environment { get; } - public TurboHTTP.Server.TurboHostBuilder Host { get; } - public Microsoft.Extensions.Logging.ILoggingBuilder Logging { get; } - public TurboHTTP.Server.TurboServerOptions Server { get; } - public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } - public TurboHTTP.Server.TurboWebApplication Build() { } - } -} -namespace TurboHTTP.Server.Middleware -{ - public sealed class TurboPipelineBuilder : TurboHTTP.Server.ITurboApplicationBuilder - { - public TurboPipelineBuilder() { } - public TurboHTTP.Server.ITurboApplicationBuilder Map(string pathPrefix, System.Action configure) { } - public TurboHTTP.Server.ITurboApplicationBuilder MapWhen(System.Func predicate, System.Action configure) { } - public TurboHTTP.Server.ITurboApplicationBuilder Run(TurboHTTP.Server.TurboRequestDelegate handler) { } - public TurboHTTP.Server.ITurboApplicationBuilder Use(System.Func middleware) { } - public TurboHTTP.Server.ITurboApplicationBuilder Use() - where T : class, TurboHTTP.Server.ITurboMiddleware { } + public static Microsoft.Extensions.Hosting.IHostBuilder UseTurboHttp(this Microsoft.Extensions.Hosting.IHostBuilder builder, System.Action? configure = null) { } } } \ No newline at end of file diff --git a/src/TurboHTTP.Benchmarks/Internal/BenchmarkComparisonReport.cs b/src/TurboHTTP.Benchmarks/Internal/BenchmarkComparisonReport.cs index 6b8dbb2c9..eb1d131ad 100644 --- a/src/TurboHTTP.Benchmarks/Internal/BenchmarkComparisonReport.cs +++ b/src/TurboHTTP.Benchmarks/Internal/BenchmarkComparisonReport.cs @@ -58,6 +58,22 @@ public static string GenerateKestrelReport( return sb.ToString(); } + /// + /// Generates a two-way markdown report comparing TurboServer (Akka.Streams) against + /// Kestrel (built-in ASP.NET Core). Results with CL=1 are shown as single-request benchmarks; + /// results with CL>1 are shown as concurrent benchmarks. + /// + public static string GenerateServerReport( + IReadOnlyList kestrelResults, + IReadOnlyList turboResults) + { + var sb = new StringBuilder(); + AppendServerHeader(sb, DateTime.UtcNow); + AppendServerVersionSections(sb, kestrelResults, turboResults); + AppendServerNotes(sb); + return sb.ToString(); + } + /// /// Writes a markdown report to benchmarks/comparison_report_{timestamp}.md /// relative to the current working directory, creating the directory if needed. @@ -556,4 +572,221 @@ private static string StripVersionSuffix(string name) private static BenchmarkResult Zero(string name) => new(name, 0, 0, 0, 0, 0); + + private static void AppendServerHeader(StringBuilder sb, DateTime reportDate) + { + sb.AppendLine("# TurboServer vs Kestrel — Server Benchmark Comparison"); + sb.AppendLine(); + sb.AppendLine("| | |"); + sb.AppendLine("|---|---|"); + sb.AppendLine($"| **Report date** | {reportDate:yyyy-MM-dd HH:mm} UTC |"); + sb.AppendLine("| **Comparison** | TurboServer (Akka.Streams) vs Kestrel (built-in) |"); + sb.AppendLine("| **Protocol** | HTTP/1.1 cleartext, HTTP/2 (h2c), HTTP/3 (QUIC+TLS) |"); + sb.AppendLine("| **Workloads** | plaintext, json, fortunes, upload (1 MB POST) |"); + sb.AppendLine("| **Client** | HttpClient (SocketsHttpHandler) — same for both servers |"); + sb.AppendLine(); + sb.AppendLine("> **Legend:**"); + sb.AppendLine("> - ✓ TurboServer faster than Kestrel by >5%"); + sb.AppendLine("> - – within ±5%"); + sb.AppendLine("> - ✗ TurboServer slower than Kestrel by >5%"); + sb.AppendLine("> - **Δ%** is relative to the Kestrel baseline (positive = TurboServer faster/cheaper)"); + sb.AppendLine(); + } + + private static void AppendServerVersionSections( + StringBuilder sb, + IReadOnlyList kestrelResults, + IReadOnlyList turboResults) + { + foreach (var version in new[] { "1.1", "2.0", "3.0" }) + { + var kestrelAll = FilterByVersion(kestrelResults, version); + var turboAll = FilterByVersion(turboResults, version); + + if (kestrelAll.Count == 0 && turboAll.Count == 0) + { + continue; + } + + sb.AppendLine($"# HTTP/{version}"); + sb.AppendLine(); + + var kestrelSingle = FilterByConcurrency(kestrelAll, cl: 1); + var turboSingle = FilterByConcurrency(turboAll, cl: 1); + + if (kestrelSingle.Count > 0 || turboSingle.Count > 0) + { + AppendServer2WayThroughputTable(sb, kestrelSingle, turboSingle, "Single Request"); + AppendServer2WayLatencyTable(sb, kestrelSingle, turboSingle, "Single Request"); + AppendServer2WayMemoryTable(sb, kestrelSingle, turboSingle, "Single Request"); + } + + var kestrelConc = FilterByConcurrencyMin(kestrelAll, 2); + var turboConc = FilterByConcurrencyMin(turboAll, 2); + + if (kestrelConc.Count > 0 || turboConc.Count > 0) + { + sb.AppendLine("---"); + sb.AppendLine(); + sb.AppendLine("## Concurrent Benchmarks"); + sb.AppendLine(); + AppendServer2WayThroughputTable(sb, kestrelConc, turboConc, "Concurrent"); + AppendServer2WayLatencyTable(sb, kestrelConc, turboConc, "Concurrent"); + AppendServer2WayMemoryTable(sb, kestrelConc, turboConc, "Concurrent"); + } + } + } + + private static void AppendServer2WayThroughputTable( + StringBuilder sb, + IReadOnlyList kestrelResults, + IReadOnlyList turboResults, + string section) + { + sb.AppendLine($"## {section} — Throughput (Req/sec — higher is better)"); + sb.AppendLine(); + sb.AppendLine("| Scenario | Kestrel | TurboServer | Δ% |"); + sb.AppendLine("|---|---:|---:|---:|"); + + foreach (var row in MatchRows2Way(kestrelResults, turboResults)) + { + var cl = ParseConcurrencyLevel(row.Name); + var kestrelRps = cl > 1 + ? ConcurrentNsToRps(row.Kestrel.MeanNanoseconds, cl) + : NsToRps(row.Kestrel.MeanNanoseconds); + var turboRps = cl > 1 + ? ConcurrentNsToRps(row.Turbo.MeanNanoseconds, cl) + : NsToRps(row.Turbo.MeanNanoseconds); + + var delta = ComputeDelta(kestrelRps, turboRps); + + sb.AppendLine( + $"| {row.Name} | {kestrelRps:N0} | {turboRps:N0} | {delta:+0.0;-0.0;0.0}% |"); + } + + sb.AppendLine(); + } + + private static void AppendServer2WayLatencyTable( + StringBuilder sb, + IReadOnlyList kestrelResults, + IReadOnlyList turboResults, + string section) + { + sb.AppendLine($"## {section} — Latency (ns — lower is better)"); + sb.AppendLine(); + + sb.AppendLine("### p50 (Median)"); + sb.AppendLine(); + sb.AppendLine("| Scenario | Kestrel | TurboServer | Δ% |"); + sb.AppendLine("|---|---:|---:|---:|"); + AppendServer2WayLatencyRows(sb, kestrelResults, turboResults, r => r.P50Nanoseconds); + sb.AppendLine(); + + sb.AppendLine("### p95"); + sb.AppendLine(); + sb.AppendLine("| Scenario | Kestrel | TurboServer | Δ% |"); + sb.AppendLine("|---|---:|---:|---:|"); + AppendServer2WayLatencyRows(sb, kestrelResults, turboResults, r => r.P95Nanoseconds); + sb.AppendLine(); + + sb.AppendLine("### p99"); + sb.AppendLine(); + sb.AppendLine("| Scenario | Kestrel | TurboServer | Δ% |"); + sb.AppendLine("|---|---:|---:|---:|"); + AppendServer2WayLatencyRows(sb, kestrelResults, turboResults, r => r.P99Nanoseconds); + sb.AppendLine(); + } + + private static void AppendServer2WayLatencyRows( + StringBuilder sb, + IReadOnlyList kestrelResults, + IReadOnlyList turboResults, + Func selector) + { + foreach (var row in MatchRows2Way(kestrelResults, turboResults)) + { + var kestrelVal = selector(row.Kestrel); + var turboVal = selector(row.Turbo); + var delta = ComputeLatencyDelta(kestrelVal, turboVal); + + sb.AppendLine( + $"| {row.Name} | {kestrelVal:N0} ns | {turboVal:N0} ns | {delta:+0.0;-0.0;0.0}% |"); + } + } + + private static void AppendServer2WayMemoryTable( + StringBuilder sb, + IReadOnlyList kestrelResults, + IReadOnlyList turboResults, + string section) + { + sb.AppendLine($"## {section} — Memory (Allocated bytes/op — lower is better)"); + sb.AppendLine(); + sb.AppendLine("| Scenario | Kestrel | TurboServer | Δ% |"); + sb.AppendLine("|---|---:|---:|---:|"); + + foreach (var row in MatchRows2Way(kestrelResults, turboResults)) + { + double kestrelBytes = row.Kestrel.AllocatedBytes; + double turboBytes = row.Turbo.AllocatedBytes; + var delta = ComputeLatencyDelta(kestrelBytes, turboBytes); + + sb.AppendLine( + $"| {row.Name} | {row.Kestrel.AllocatedBytes:N0} B | {row.Turbo.AllocatedBytes:N0} B | {delta:+0.0;-0.0;0.0}% |"); + } + + sb.AppendLine(); + } + + private static void AppendServerNotes(StringBuilder sb) + { + sb.AppendLine("## Notes"); + sb.AppendLine(); + sb.AppendLine("- Both servers run the same ASP.NET Core middleware pipeline (same MapGet/MapPost endpoints)."); + sb.AppendLine("- Client is standard HttpClient (SocketsHttpHandler) — identical for both servers."); + sb.AppendLine("- HTTP/1.1 and HTTP/2 use cleartext (no TLS). HTTP/3 uses QUIC+TLS with a self-signed certificate."); + sb.AppendLine("- All requests target loopback (127.0.0.1) — results reflect pure server overhead."); + sb.AppendLine("- Memory figures reflect managed allocations only; native/pooled buffers are not included."); + sb.AppendLine("- TurboServer uses Akka.Streams for connection handling; Kestrel uses its built-in IO pipeline."); + sb.AppendLine(); + } + + private static IReadOnlyList<(string Name, BenchmarkResult Kestrel, BenchmarkResult Turbo)> + MatchRows2Way( + IReadOnlyList kestrelResults, + IReadOnlyList turboResults) + { + var kestrelMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var k in kestrelResults) + { + kestrelMap[k.BenchmarkName] = k; + } + + var result = new List<(string, BenchmarkResult, BenchmarkResult)>(); + var matchedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var turbo in turboResults) + { + if (!matchedNames.Add(turbo.BenchmarkName)) + { + continue; + } + + var name = turbo.BenchmarkName; + var kestrel = kestrelMap.TryGetValue(name, out var k) ? k : Zero(name); + result.Add((StripVersionSuffix(name), kestrel, turbo)); + } + + foreach (var kestrel in kestrelResults) + { + if (!matchedNames.Contains(kestrel.BenchmarkName)) + { + matchedNames.Add(kestrel.BenchmarkName); + result.Add((StripVersionSuffix(kestrel.BenchmarkName), kestrel, Zero(kestrel.BenchmarkName))); + } + } + + return result; + } } diff --git a/src/TurboHTTP.Benchmarks/Internal/BenchmarkData.cs b/src/TurboHTTP.Benchmarks/Internal/BenchmarkData.cs new file mode 100644 index 000000000..0853ed500 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Internal/BenchmarkData.cs @@ -0,0 +1,42 @@ +namespace TurboHTTP.Benchmarks.Internal; + +public static class BenchmarkData +{ + public sealed record FortuneRow(int Id, string Message); + + private static readonly FortuneRow[] Rows = GenerateRows(10_000); + + public static readonly string FortunesHtml = GenerateFortunesHtml(); + + public static FortuneRow GetRandomRow() + { + var index = Random.Shared.Next(0, Rows.Length); + return Rows[index]; + } + + private static FortuneRow[] GenerateRows(int count) + { + var rows = new FortuneRow[count]; + for (var i = 0; i < count; i++) + { + rows[i] = new FortuneRow(i + 1, string.Concat("Fortune #", (i + 1).ToString())); + } + return rows; + } + + private static string GenerateFortunesHtml() + { + var sb = new System.Text.StringBuilder(2 * 1024); + sb.Append("Fortunes"); + for (var i = 0; i < 25; i++) + { + sb.Append(""); + } + sb.Append("
idmessage
"); + sb.Append(i + 1); + sb.Append(""); + sb.Append(string.Concat("Fortune #", (i + 1).ToString())); + sb.Append("
"); + return sb.ToString(); + } +} diff --git a/src/TurboHTTP.Benchmarks/Internal/BenchmarkRoutes.cs b/src/TurboHTTP.Benchmarks/Internal/BenchmarkRoutes.cs new file mode 100644 index 000000000..b17a04d6b --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Internal/BenchmarkRoutes.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace TurboHTTP.Benchmarks.Internal; + +public static class BenchmarkRoutes +{ + public static void Register(WebApplication app) + { + app.MapGet("/benchmark/simple", () => + Results.Content("OK\n", "text/plain")); + + app.MapPost("/benchmark/payload", async ctx => + { + using var ms = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(ms); + var received = ms.ToArray(); + var response = System.Text.Encoding.UTF8.GetBytes( + string.Concat("received:", received.Length.ToString())); + ctx.Response.ContentType = "text/plain"; + ctx.Response.ContentLength = response.Length; + await ctx.Response.Body.WriteAsync(response); + }); + + app.MapGet("/plaintext", () => + Results.Content("Hello, World!", "text/plain")); + + app.MapGet("/json", () => + Results.Json(new { message = "Hello, World!" })); + + app.MapGet("/db", () => + { + var row = BenchmarkData.GetRandomRow(); + return Results.Json(row); + }); + + app.MapGet("/queries", (int? queries) => + { + var count = Math.Clamp(queries ?? 1, 1, 500); + var rows = new BenchmarkData.FortuneRow[count]; + for (var i = 0; i < count; i++) + { + rows[i] = BenchmarkData.GetRandomRow(); + } + return Results.Json(rows); + }); + + app.MapGet("/fortunes", () => + Results.Content(BenchmarkData.FortunesHtml, "text/html; charset=utf-8")); + + app.MapPost("/echo", async ctx => + { + ctx.Response.ContentType = ctx.Request.ContentType ?? "application/octet-stream"; + ctx.Response.ContentLength = ctx.Request.ContentLength; + await ctx.Request.Body.CopyToAsync(ctx.Response.Body); + }); + + app.MapPost("/upload", async ctx => + { + long count = 0; + var buffer = new byte[64 * 1024]; + int read; + while ((read = await ctx.Request.Body.ReadAsync(buffer)) > 0) + { + count += read; + } + var response = string.Concat("received:", count.ToString()); + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(response); + }); + } +} diff --git a/src/TurboHTTP.Benchmarks/Internal/BenchmarkServer.cs b/src/TurboHTTP.Benchmarks/Internal/BenchmarkServer.cs index 63998fb9f..24208cd50 100644 --- a/src/TurboHTTP.Benchmarks/Internal/BenchmarkServer.cs +++ b/src/TurboHTTP.Benchmarks/Internal/BenchmarkServer.cs @@ -5,40 +5,23 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace TurboHTTP.Benchmarks.Internal; -/// -/// Minimal Kestrel test server for benchmarking both HttpClient and TurboHttp. -/// Binds three dynamic ports: HTTP/1.1, HTTP/2 cleartext (h2c), and HTTP/3 (QUIC+TLS). -/// Exposes two simple benchmark routes with keep-alive enabled. -/// public sealed class BenchmarkServer : IAsyncDisposable { private WebApplication? _app; private X509Certificate2? _cert; - /// Port on which the HTTP/1.1 listener is listening. Set after initialization. public int Http11Port { get; private set; } - /// Port on which the HTTP/2 cleartext (h2c) listener is listening. Set after initialization. public int Http20Port { get; private set; } - /// Port on which the HTTP/3 (QUIC+TLS) listener is listening. Set after initialization. public int Http30Port { get; private set; } - /// - /// Starts the Kestrel server on 127.0.0.1:0 (dynamic port) for each protocol. - /// HTTP/1.1 and HTTP/2 use separate ports because HTTP/2 cleartext (h2c) requires - /// an exclusive listener — Kestrel ignores h2c prior - /// knowledge on combined Http1AndHttp2 endpoints without TLS. - /// HTTP/3 requires TLS (QUIC mandates TLS 1.3), so a self-signed certificate is used. - /// Call this once in GlobalSetup. - /// public async ValueTask InitializeAsync() { _cert = GenerateSelfSignedCert(); @@ -94,10 +77,6 @@ public async ValueTask InitializeAsync() _app = app; } - /// - /// Stops the server and cleans up resources. - /// Call this in GlobalCleanup. - /// public async ValueTask DisposeAsync() { if (_app is not null) @@ -124,20 +103,6 @@ private static X509Certificate2 GenerateSelfSignedCert() private static void RegisterRoutes(WebApplication app) { - // Simple benchmark endpoint: minimal response body, suitable for throughput testing - app.MapGet("/benchmark/simple", () => - Results.Content("OK\n", "text/plain")); - - // Payload echo endpoint: accepts POST body and returns size received - app.MapPost("/benchmark/payload", async ctx => - { - using var ms = new MemoryStream(); - await ctx.Request.Body.CopyToAsync(ms); - var received = ms.ToArray(); - var response = System.Text.Encoding.UTF8.GetBytes($"received:{received.Length}"); - ctx.Response.ContentType = "text/plain"; - ctx.Response.ContentLength = response.Length; - await ctx.Response.Body.WriteAsync(response); - }); + BenchmarkRoutes.Register(app); } } diff --git a/src/TurboHTTP.Benchmarks/Internal/ClientHelper.cs b/src/TurboHTTP.Benchmarks/Internal/ClientHelper.cs index 175471d8b..2c65d19e5 100644 --- a/src/TurboHTTP.Benchmarks/Internal/ClientHelper.cs +++ b/src/TurboHTTP.Benchmarks/Internal/ClientHelper.cs @@ -43,7 +43,7 @@ public static ClientHelper CreateClient(Uri baseAddress, Version version) Http1 = new Http1Options { MaxConnectionsPerServer = 512, - MaxPipelineDepth = 2 + MaxPipelineDepth = 64 }, // H2: 16 connections × 1000 streams = 16 000 in-flight capacity. Http2 = new Http2Options @@ -82,7 +82,7 @@ public static ClientHelper CreateStreamingClient(Uri baseAddress, Version versio BaseAddress = baseAddress, DangerousAcceptAnyServerCertificate = true, // Streaming: fewer connections but deep pipelining via the channel. - Http1 = new Http1Options { MaxConnectionsPerServer = 4, MaxPipelineDepth = 2048 }, + Http1 = new Http1Options { MaxConnectionsPerServer = 4, MaxPipelineDepth = 2 * 1024 }, // H2: 16 connections × 1000 streams for high-CL streaming. Http2 = new Http2Options { MaxConnectionsPerServer = 16, MaxConcurrentStreams = 1000 }, // H3: 8 connections × 1000 streams, larger QPACK table for repeated header patterns. diff --git a/src/TurboHTTP.Benchmarks/Internal/KestrelBaseClass.cs b/src/TurboHTTP.Benchmarks/Internal/KestrelBaseClass.cs index 8d8d19d52..081d66fc7 100644 --- a/src/TurboHTTP.Benchmarks/Internal/KestrelBaseClass.cs +++ b/src/TurboHTTP.Benchmarks/Internal/KestrelBaseClass.cs @@ -48,6 +48,11 @@ public abstract class KestrelBaseClass : BenchmarkSuiteBase ///
public Uri HeavyUri => new($"{Scheme}://127.0.0.1:{KestrelPort}/benchmark/payload"); + public Uri PlaintextUri => new($"{Scheme}://127.0.0.1:{KestrelPort}/plaintext"); + public Uri JsonUri => new($"{Scheme}://127.0.0.1:{KestrelPort}/json"); + public Uri FortunesUri => new($"{Scheme}://127.0.0.1:{KestrelPort}/fortunes"); + public Uri UploadUri => new($"{Scheme}://127.0.0.1:{KestrelPort}/upload"); + /// /// Returns the base address for the Kestrel test server at the current HTTP version port. /// diff --git a/src/TurboHTTP.Benchmarks/Internal/TurboBenchmarkServer.cs b/src/TurboHTTP.Benchmarks/Internal/TurboBenchmarkServer.cs new file mode 100644 index 000000000..57051d645 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Internal/TurboBenchmarkServer.cs @@ -0,0 +1,90 @@ +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TurboHTTP.Server; + +namespace TurboHTTP.Benchmarks.Internal; + +public sealed class TurboBenchmarkServer : IAsyncDisposable +{ + private WebApplication? _app; + private X509Certificate2? _cert; + + public int Http11Port { get; private set; } + public int Http20Port { get; private set; } + public int Http30Port { get; private set; } + + public async ValueTask InitializeAsync() + { + _cert = GenerateSelfSignedCert(); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var cert = _cert; + builder.Host.UseTurboHttp(options => + { + options.Listen(IPAddress.Loopback, 0, lo => + lo.Protocols = HttpProtocols.Http1); + + options.Listen(IPAddress.Loopback, 0, lo => + lo.Protocols = HttpProtocols.Http2); + + options.Listen(IPAddress.Loopback, 0, lo => + { + lo.Protocols = HttpProtocols.Http3; + lo.UseHttps(cert); + }); + + options.Http2.MaxConcurrentStreams = 512; + options.Http2.InitialConnectionWindowSize = 4 * 1024 * 1024; + options.Http2.InitialStreamWindowSize = 1 * 1024 * 1024; + }); + + var app = builder.Build(); + + BenchmarkRoutes.Register(app); + + await app.StartAsync(); + + var addresses = app.Services.GetRequiredService() + .Features.Get()! + .Addresses + .ToArray(); + + Http11Port = new Uri(addresses[0]).Port; + Http20Port = new Uri(addresses[1]).Port; + Http30Port = new Uri(addresses[2]).Port; + + _app = app; + } + + public async ValueTask DisposeAsync() + { + if (_app is not null) + { + await _app.StopAsync(); + await _app.DisposeAsync(); + } + + _cert?.Dispose(); + } + + private static X509Certificate2 GenerateSelfSignedCert() + { + using var key = RSA.Create(2048); + var san = new SubjectAlternativeNameBuilder(); + san.AddDnsName("localhost"); + san.AddIpAddress(IPAddress.Loopback); + var request = new CertificateRequest( + "CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(san.Build()); + var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); + return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), null); + } +} diff --git a/src/TurboHTTP.Benchmarks/Internal/TurboServerBaseClass.cs b/src/TurboHTTP.Benchmarks/Internal/TurboServerBaseClass.cs new file mode 100644 index 000000000..780a1c185 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Internal/TurboServerBaseClass.cs @@ -0,0 +1,81 @@ +namespace TurboHTTP.Benchmarks.Internal; + +public abstract class TurboServerBaseClass : BenchmarkSuiteBase +{ + private static TurboBenchmarkServer? _sharedServer; + private static readonly SemaphoreSlim _serverLock = new(1, 1); + private static int _serverRefCount; + + protected static readonly byte[] HeavyPayload = GeneratePayload(1 * 1024 * 1024); + + protected int TurboHttp11Port { get; private set; } + protected int TurboHttp20Port { get; private set; } + protected int TurboHttp30Port { get; private set; } + + protected int TurboPort => HttpVersion switch + { + "3.0" => TurboHttp30Port, + "2.0" => TurboHttp20Port, + _ => TurboHttp11Port, + }; + + private string Scheme => HttpVersion == "3.0" ? "https" : "http"; + + public Uri PlaintextUri => new(string.Concat(Scheme, "://127.0.0.1:", TurboPort.ToString(), "/plaintext")); + public Uri JsonUri => new(string.Concat(Scheme, "://127.0.0.1:", TurboPort.ToString(), "/json")); + public Uri FortunesUri => new(string.Concat(Scheme, "://127.0.0.1:", TurboPort.ToString(), "/fortunes")); + public Uri UploadUri => new(string.Concat(Scheme, "://127.0.0.1:", TurboPort.ToString(), "/upload")); + public Uri BaseAddress => new(string.Concat(Scheme, "://127.0.0.1:", TurboPort.ToString())); + + public static byte[] GeneratePayload(int sizeBytes) + { + var payload = new byte[sizeBytes]; + for (var i = 0; i < sizeBytes; i++) + { + payload[i] = (byte)(i % 256); + } + return payload; + } + + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + await _serverLock.WaitAsync(); + try + { + if (_sharedServer is null) + { + _sharedServer = new TurboBenchmarkServer(); + await _sharedServer.InitializeAsync(); + } + + _serverRefCount++; + TurboHttp11Port = _sharedServer.Http11Port; + TurboHttp20Port = _sharedServer.Http20Port; + TurboHttp30Port = _sharedServer.Http30Port; + } + finally + { + _serverLock.Release(); + } + } + + public override async Task GlobalCleanup() + { + await _serverLock.WaitAsync(); + try + { + _serverRefCount--; + if (_serverRefCount == 0 && _sharedServer is not null) + { + await _sharedServer.DisposeAsync(); + _sharedServer = null; + } + } + finally + { + _serverLock.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Program.cs b/src/TurboHTTP.Benchmarks/Program.cs index 36d13ed4f..f7d8f5677 100644 --- a/src/TurboHTTP.Benchmarks/Program.cs +++ b/src/TurboHTTP.Benchmarks/Program.cs @@ -66,3 +66,56 @@ Console.WriteLine($" KestrelTurboSendAsyncConcurrentBenchmarks : {(kestrelTurboSend is not null ? "OK" : "MISSING")}"); Console.WriteLine($" KestrelTurboStreamingConcurrentBenchmarks : {(kestrelTurboStream is not null ? "OK" : "MISSING")}"); } + +var kestrelServerPlaintext = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var turboServerPlaintext = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var kestrelServerJson = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var turboServerJson = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var kestrelServerFortunes = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var turboServerFortunes = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var kestrelServerUpload = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); +var turboServerUpload = enumerable.FirstOrDefault(s => + s.HasBenchmarksOf()); + +var hasAnyServerBenchmarks = + kestrelServerPlaintext is not null || turboServerPlaintext is not null || + kestrelServerJson is not null || turboServerJson is not null || + kestrelServerFortunes is not null || turboServerFortunes is not null || + kestrelServerUpload is not null || turboServerUpload is not null; + +if (hasAnyServerBenchmarks) +{ + var kestrelServerResults = new List(); + var turboServerResults = new List(); + + if (kestrelServerPlaintext is not null) kestrelServerResults.AddRange(SummaryExtractor.Extract(kestrelServerPlaintext)); + if (kestrelServerJson is not null) kestrelServerResults.AddRange(SummaryExtractor.Extract(kestrelServerJson)); + if (kestrelServerFortunes is not null) kestrelServerResults.AddRange(SummaryExtractor.Extract(kestrelServerFortunes)); + if (kestrelServerUpload is not null) kestrelServerResults.AddRange(SummaryExtractor.Extract(kestrelServerUpload)); + + if (turboServerPlaintext is not null) turboServerResults.AddRange(SummaryExtractor.Extract(turboServerPlaintext)); + if (turboServerJson is not null) turboServerResults.AddRange(SummaryExtractor.Extract(turboServerJson)); + if (turboServerFortunes is not null) turboServerResults.AddRange(SummaryExtractor.Extract(turboServerFortunes)); + if (turboServerUpload is not null) turboServerResults.AddRange(SummaryExtractor.Extract(turboServerUpload)); + + var serverMarkdown = BenchmarkComparisonReport.GenerateServerReport(kestrelServerResults, turboServerResults); + + if (serverMarkdown.Contains("NaN") || serverMarkdown.Contains("Infinity") || serverMarkdown.Contains("Inf%")) + { + Console.Error.WriteLine("WARNING: Server report contains NaN or Inf values — check input data."); + } + + var serverPath = BenchmarkComparisonReport.WriteReportToFile(serverMarkdown); + Console.WriteLine($"Server comparison report: {serverPath}"); +} +else +{ + Console.WriteLine("Server comparison report skipped — no server benchmark suites ran."); +} diff --git a/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerFortunesBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerFortunesBenchmark.cs new file mode 100644 index 000000000..a1e0f8b13 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerFortunesBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Kestrel; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class KestrelServerFortunesBenchmark : KestrelBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Fortunes_Sequential() + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Fortunes_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerJsonBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerJsonBenchmark.cs new file mode 100644 index 000000000..7bb837883 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerJsonBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Kestrel; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class KestrelServerJsonBenchmark : KestrelBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Json_Sequential() + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Json_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerPlaintextBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerPlaintextBenchmark.cs new file mode 100644 index 000000000..4bb698dd0 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerPlaintextBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Kestrel; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class KestrelServerPlaintextBenchmark : KestrelBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Plaintext_Sequential() + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Plaintext_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerUploadBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerUploadBenchmark.cs new file mode 100644 index 000000000..ab7696f0b --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Kestrel/KestrelServerUploadBenchmark.cs @@ -0,0 +1,94 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Kestrel; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class KestrelServerUploadBenchmark : KestrelBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Upload_Sequential() + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Upload_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerFortunesBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerFortunesBenchmark.cs new file mode 100644 index 000000000..be0c3472b --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerFortunesBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Turbo; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class TurboServerFortunesBenchmark : TurboServerBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Fortunes_Sequential() + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Fortunes_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(FortunesUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerJsonBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerJsonBenchmark.cs new file mode 100644 index 000000000..70348c454 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerJsonBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Turbo; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class TurboServerJsonBenchmark : TurboServerBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Json_Sequential() + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Json_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(JsonUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerPlaintextBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerPlaintextBenchmark.cs new file mode 100644 index 000000000..339112998 --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerPlaintextBenchmark.cs @@ -0,0 +1,91 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Turbo; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class TurboServerPlaintextBenchmark : TurboServerBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Plaintext_Sequential() + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Plaintext_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var response = await _httpClient.GetAsync(PlaintextUri); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerUploadBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerUploadBenchmark.cs new file mode 100644 index 000000000..cbfe2c99a --- /dev/null +++ b/src/TurboHTTP.Benchmarks/Server/Turbo/TurboServerUploadBenchmark.cs @@ -0,0 +1,94 @@ +using BenchmarkDotNet.Attributes; +using TurboHTTP.Benchmarks.Internal; + +namespace TurboHTTP.Benchmarks.Server.Turbo; + +[MemoryDiagnoser] +[WarmupCount(3)] +[IterationCount(10)] +public class TurboServerUploadBenchmark : TurboServerBaseClass +{ + private const int MaxFanOut = 1024; + + [Params(1, 64, 256)] + public int ConcurrencyLevel { get; set; } + + private HttpClient _httpClient = null!; + private Task[] _tasks = null!; + private SemaphoreSlim _fanOutGate = null!; + + [GlobalSetup] + public override async Task GlobalSetup() + { + await base.GlobalSetup(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var handler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + EnableMultipleHttp2Connections = true, + MaxConnectionsPerServer = 128, + SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }, + }; + + _httpClient = new HttpClient(handler) + { + DefaultRequestVersion = HttpVersionValue, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + _tasks = new Task[ConcurrencyLevel]; + _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); + await WarmupRequest(); + } + + [GlobalCleanup] + public override async Task GlobalCleanup() + { + _fanOutGate.Dispose(); + _httpClient.Dispose(); + await base.GlobalCleanup(); + } + + public override async Task WarmupRequest() + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + public async Task Upload_Sequential() + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + + [Benchmark] + [BenchmarkCategory("Concurrent")] + public Task Upload_Concurrent() + { + for (var i = 0; i < ConcurrencyLevel; i++) + { + _tasks[i] = SendRequest(); + } + return Task.WhenAll(_tasks); + } + + private async Task SendRequest() + { + await _fanOutGate.WaitAsync(); + try + { + using var content = new ByteArrayContent(HeavyPayload); + using var response = await _httpClient.PostAsync(UploadUri, content); + response.EnsureSuccessStatusCode(); + } + finally + { + _fanOutGate.Release(); + } + } +} diff --git a/src/TurboHTTP.Benchmarks/Server/TurboServerThroughputBenchmark.cs b/src/TurboHTTP.Benchmarks/Server/TurboServerThroughputBenchmark.cs deleted file mode 100644 index 771c44b7a..000000000 --- a/src/TurboHTTP.Benchmarks/Server/TurboServerThroughputBenchmark.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System.Net; -using System.Text.Json; -using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using TurboHTTP.Benchmarks.Internal; - -namespace TurboHTTP.Benchmarks.Server; - -/// -/// End-to-end server throughput benchmark for TurboHTTP running on loopback. -/// Measures request/second throughput with various concurrency levels and payload sizes. -/// Starts a minimal TurboHTTP server with simple routes and fires concurrent requests at it using HttpClient. -/// -[Config(typeof(EngineBenchmarkConfig))] -[MemoryDiagnoser] -[WarmupCount(5)] -[IterationCount(15)] -public class TurboServerThroughputBenchmark -{ - private const int MaxFanOut = 1024; - - [Params(1, 64, 256)] - public int ConcurrencyLevel { get; set; } - - private WebApplication? _app; - private HttpClient? _httpClient; - private Task[] _tasks = null!; - private SemaphoreSlim _fanOutGate = null!; - private int _serverPort; - - /// Base address for requests against the TurboHTTP server. - private Uri ServerUri => new($"http://127.0.0.1:{_serverPort}"); - - /// Plaintext endpoint returning minimal response. - private Uri PlaintextUri => new(ServerUri, "/plaintext"); - - /// JSON endpoint returning small JSON object. - private Uri JsonUri => new(ServerUri, "/json"); - - [GlobalSetup] - public async Task GlobalSetup() - { - // Configure ThreadPool for high concurrency - ThreadPool.GetMinThreads(out var w, out var io); - ThreadPool.SetMinThreads(Math.Max(w, 1024), Math.Max(io, 1024)); - - // Enable HTTP/2 over cleartext for benchmarks - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - - // Start the TurboHTTP server - await StartServerAsync(); - - // Create HttpClient for requests - var handler = new SocketsHttpHandler - { - AllowAutoRedirect = false, - EnableMultipleHttp2Connections = true, - MaxConnectionsPerServer = 128, - }; - - _httpClient = new HttpClient(handler) - { - DefaultRequestVersion = System.Net.HttpVersion.Version11, - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact, - }; - - _tasks = new Task[ConcurrencyLevel]; - _fanOutGate = new SemaphoreSlim(MaxFanOut, MaxFanOut); - - // Warmup request to initialize connection pool and JIT - await WarmupRequest(); - } - - [GlobalCleanup] - public async Task GlobalCleanup() - { - _fanOutGate?.Dispose(); - _httpClient?.Dispose(); - await StopServerAsync(); - } - - /// - /// Sequential GET /plaintext: measures single-request latency. - /// - [Benchmark] - public async Task PlaintextGet_Sequential() - { - using var response = await _httpClient!.GetAsync(PlaintextUri); - response.EnsureSuccessStatusCode(); - } - - /// - /// Concurrent GET /plaintext with 64 parallel requests: measures throughput. - /// - [Benchmark] - [BenchmarkCategory("Concurrent")] - public Task PlaintextGet_Concurrent() - { - for (var i = 0; i < ConcurrencyLevel; i++) - { - _tasks[i] = SendPlaintextRequest(); - } - - return Task.WhenAll(_tasks); - } - - /// - /// Sequential GET /json: measures latency on small JSON response. - /// - [Benchmark] - public async Task JsonGet_Sequential() - { - using var response = await _httpClient!.GetAsync(JsonUri); - response.EnsureSuccessStatusCode(); - } - - /// - /// Concurrent GET /json with variable concurrency level: measures throughput on JSON responses. - /// - [Benchmark] - [BenchmarkCategory("Concurrent")] - public Task JsonGet_Concurrent() - { - for (var i = 0; i < ConcurrencyLevel; i++) - { - _tasks[i] = SendJsonRequest(); - } - - return Task.WhenAll(_tasks); - } - - private async Task SendPlaintextRequest() - { - await _fanOutGate.WaitAsync(); - try - { - using var response = await _httpClient!.GetAsync(PlaintextUri); - response.EnsureSuccessStatusCode(); - } - finally - { - _fanOutGate.Release(); - } - } - - private async Task SendJsonRequest() - { - await _fanOutGate.WaitAsync(); - try - { - using var response = await _httpClient!.GetAsync(JsonUri); - response.EnsureSuccessStatusCode(); - } - finally - { - _fanOutGate.Release(); - } - } - - private async Task WarmupRequest() - { - using var response = await _httpClient!.GetAsync(PlaintextUri); - response.EnsureSuccessStatusCode(); - } - - private async Task StartServerAsync() - { - var builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - - var app = builder.Build(); - - // Register benchmark endpoints - app.MapGet("/plaintext", () => - Results.Content("Hello, World!", "text/plain")); - - app.MapGet("/json", () => - Results.Json(new { message = "Hello, World!" })); - - await app.StartAsync(); - - // Extract the dynamically assigned port - var addresses = app.Services.GetRequiredService() - .Features.Get()! - .Addresses - .FirstOrDefault(); - - if (addresses is null) - { - throw new InvalidOperationException("Failed to extract server address"); - } - - _serverPort = new Uri(addresses).Port; - _app = app; - } - - private async Task StopServerAsync() - { - if (_app is not null) - { - await _app.StopAsync(); - await _app.DisposeAsync(); - } - } -} diff --git a/src/TurboHTTP.IntegrationTests.Client/Features/SseFeatureSpec.cs b/src/TurboHTTP.IntegrationTests.Client/Features/SseFeatureSpec.cs index cb9b98e0b..9fd400dba 100644 --- a/src/TurboHTTP.IntegrationTests.Client/Features/SseFeatureSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/Features/SseFeatureSpec.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using TurboHTTP.Client; -using TurboHTTP.Features.Sse; +using Servus.Akka.Sse; using TurboHTTP.IntegrationTests.Client.Shared; using TurboHTTP.Tests.Shared; diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/ConcurrencySpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/ConcurrencySpec.cs index 5bb48fc80..f4608b2d1 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/ConcurrencySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/ConcurrencySpec.cs @@ -12,7 +12,7 @@ public ConcurrencySpec(ServerContainerFixture server, ActorSystemFixture systemF { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Fact(Timeout = 30000)] public async Task Concurrency_should_succeed_with_parallel_gets() diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/ConnectionSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/ConnectionSpec.cs index 246167923..3f727bd14 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/ConnectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/ConnectionSpec.cs @@ -14,7 +14,7 @@ public ConnectionSpec(ServerContainerFixture server, ActorSystemFixture systemFi { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Fact(Timeout = 15000)] public async Task Connection_should_complete_single_request_response_cycle() diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/EncodingSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/EncodingSpec.cs index 77c8b9d71..085b4f642 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/EncodingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/EncodingSpec.cs @@ -13,7 +13,7 @@ public EncodingSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Fact(Timeout = 15000)] public async Task Encoding_should_decompress_gzip_response() diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/HeaderSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/HeaderSpec.cs index d226deb8a..839040fd1 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/HeaderSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/HeaderSpec.cs @@ -13,7 +13,7 @@ public HeaderSpec(ServerContainerFixture server, ActorSystemFixture systemFixtur { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Fact(Timeout = 15000)] public async Task Header_should_forward_custom_header() diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/SmokeSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/SmokeSpec.cs index 48ced4d75..7f5da880c 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/SmokeSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/SmokeSpec.cs @@ -14,7 +14,7 @@ public SmokeSpec(ServerContainerFixture server, ActorSystemFixture systemFixture { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Fact(Timeout = 15000)] public async Task Get_should_return_200() diff --git a/src/TurboHTTP.IntegrationTests.Client/H10/TransferSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H10/TransferSpec.cs index 451cc1329..390a737a4 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H10/TransferSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H10/TransferSpec.cs @@ -13,7 +13,7 @@ public TransferSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H10, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H10, tls: false); [Theory(Timeout = 15000)] [InlineData(128)] diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/ConcurrencySpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/ConcurrencySpec.cs index 44878a0ec..ad05dbc38 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/ConcurrencySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/ConcurrencySpec.cs @@ -12,7 +12,7 @@ public ConcurrencySpec(ServerContainerFixture server, ActorSystemFixture systemF { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Fact(Timeout = 30000)] public async Task Concurrency_should_succeed_with_parallel_gets() diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/ConnectionSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/ConnectionSpec.cs index cd3c32aee..fa7714686 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/ConnectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/ConnectionSpec.cs @@ -14,7 +14,7 @@ public ConnectionSpec(ServerContainerFixture server, ActorSystemFixture systemFi { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Fact(Timeout = 15000)] public async Task Connection_should_allow_sequential_requests_on_same_client() diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/EncodingSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/EncodingSpec.cs index 83e9ebc90..76c69c9e8 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/EncodingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/EncodingSpec.cs @@ -13,7 +13,7 @@ public EncodingSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Fact(Timeout = 15000)] public async Task Encoding_should_decompress_gzip_response() diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/HeaderSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/HeaderSpec.cs index a76028e86..e5e853e78 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/HeaderSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/HeaderSpec.cs @@ -13,7 +13,7 @@ public HeaderSpec(ServerContainerFixture server, ActorSystemFixture systemFixtur { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Fact(Timeout = 15000)] public async Task Header_should_forward_custom_header() diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/SmokeSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/SmokeSpec.cs index 51c75a2ca..feebce9e5 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/SmokeSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/SmokeSpec.cs @@ -14,7 +14,7 @@ public SmokeSpec(ServerContainerFixture server, ActorSystemFixture systemFixture { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Fact(Timeout = 30000)] public async Task Get_should_return_200() diff --git a/src/TurboHTTP.IntegrationTests.Client/H11/TransferSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H11/TransferSpec.cs index f459f0ea4..c1431113d 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H11/TransferSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H11/TransferSpec.cs @@ -14,7 +14,7 @@ public TransferSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H11, Tls: false); + protected override ProtocolVariant Variant => new(TestHttpVersion.H11, tls: false); [Theory(Timeout = 15000)] [InlineData(128)] @@ -150,7 +150,7 @@ public async Task Transfer_should_handle_sequential_large_bodies() [InlineData(102400)] public async Task Transfer_should_receive_large_body_over_tls(int size) { - await using var helper = CreateClient(new ProtocolVariant(TestHttpVersion.H11, Tls: true)); + await using var helper = CreateClient(new ProtocolVariant(TestHttpVersion.H11, tls: true)); var response = await helper.Client.SendAsync( new HttpRequestMessage(HttpMethod.Get, $"/bytes/{size}"), CancellationToken); @@ -163,7 +163,7 @@ public async Task Transfer_should_receive_large_body_over_tls(int size) [Fact(Timeout = 15000)] public async Task Transfer_should_receive_streaming_response_over_tls() { - await using var helper = CreateClient(new ProtocolVariant(TestHttpVersion.H11, Tls: true)); + await using var helper = CreateClient(new ProtocolVariant(TestHttpVersion.H11, tls: true)); var response = await helper.Client.SendAsync( new HttpRequestMessage(HttpMethod.Get, "/stream/3"), CancellationToken); diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/ConcurrencySpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/ConcurrencySpec.cs index 6b548980c..ed4289cc7 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/ConcurrencySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/ConcurrencySpec.cs @@ -13,7 +13,7 @@ public ConcurrencySpec(ServerContainerFixture server, ActorSystemFixture systemF { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Fact(Timeout = 30000)] public async Task Concurrency_should_multiplex_parallel_gets() diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/ConnectionSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/ConnectionSpec.cs index 8f36379e0..2f5d5e5a9 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/ConnectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/ConnectionSpec.cs @@ -14,7 +14,7 @@ public ConnectionSpec(ServerContainerFixture server, ActorSystemFixture systemFi { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Fact(Timeout = 15000)] public async Task Connection_should_reuse_for_sequential_requests() diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/EncodingSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/EncodingSpec.cs index 7940cd15a..3974a6958 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/EncodingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/EncodingSpec.cs @@ -13,7 +13,7 @@ public EncodingSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Fact(Timeout = 15000)] public async Task Encoding_should_decompress_gzip_response() diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/HeaderSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/HeaderSpec.cs index 0ca588cbc..203899073 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/HeaderSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/HeaderSpec.cs @@ -13,7 +13,7 @@ public HeaderSpec(ServerContainerFixture server, ActorSystemFixture systemFixtur { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Fact(Timeout = 15000)] public async Task Header_should_forward_custom_header() diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/SmokeSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/SmokeSpec.cs index 87cbd5428..934d0e3a5 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/SmokeSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/SmokeSpec.cs @@ -14,7 +14,7 @@ public SmokeSpec(ServerContainerFixture server, ActorSystemFixture systemFixture { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Fact(Timeout = 30000)] public async Task Get_should_return_200() diff --git a/src/TurboHTTP.IntegrationTests.Client/H2/TransferSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H2/TransferSpec.cs index 0037fe261..a55abff6b 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H2/TransferSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H2/TransferSpec.cs @@ -14,7 +14,7 @@ public TransferSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H2, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H2, tls: true); [Theory(Timeout = 15000)] [InlineData(128)] diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/ConcurrencySpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/ConcurrencySpec.cs index f36c764a1..07c82ec8c 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/ConcurrencySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/ConcurrencySpec.cs @@ -13,7 +13,7 @@ public ConcurrencySpec(ServerContainerFixture server, ActorSystemFixture systemF { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Fact(Timeout = 30000)] public async Task Concurrency_should_multiplex_parallel_gets() diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/ConnectionSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/ConnectionSpec.cs index c91b1f22e..9405f86b9 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/ConnectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/ConnectionSpec.cs @@ -14,7 +14,7 @@ public ConnectionSpec(ServerContainerFixture server, ActorSystemFixture systemFi { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Fact(Timeout = 15000)] public async Task Connection_should_reuse_for_sequential_requests() diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/EncodingSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/EncodingSpec.cs index d239d1855..f01c401fd 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/EncodingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/EncodingSpec.cs @@ -13,7 +13,7 @@ public EncodingSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Fact(Timeout = 15000)] public async Task Encoding_should_decompress_gzip_response() diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/HeaderSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/HeaderSpec.cs index 0ace7869b..feb1124e4 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/HeaderSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/HeaderSpec.cs @@ -12,7 +12,7 @@ public HeaderSpec(ServerContainerFixture server, ActorSystemFixture systemFixtur { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Fact(Timeout = 15000)] public async Task Header_should_forward_custom_header() diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/SmokeSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/SmokeSpec.cs index 41480cd84..c42ef32de 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/SmokeSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/SmokeSpec.cs @@ -14,7 +14,7 @@ public SmokeSpec(ServerContainerFixture server, ActorSystemFixture systemFixture { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Fact(Timeout = 20000)] public async Task Get_should_return_200() diff --git a/src/TurboHTTP.IntegrationTests.Client/H3/TransferSpec.cs b/src/TurboHTTP.IntegrationTests.Client/H3/TransferSpec.cs index 715fe2d0f..6966839c7 100644 --- a/src/TurboHTTP.IntegrationTests.Client/H3/TransferSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Client/H3/TransferSpec.cs @@ -14,7 +14,7 @@ public TransferSpec(ServerContainerFixture server, ActorSystemFixture systemFixt { } - protected override ProtocolVariant Variant => new(TestHttpVersion.H3, Tls: true); + protected override ProtocolVariant Variant => new(TestHttpVersion.H3, tls: true); [Theory(Timeout = 15000)] [InlineData(128)] diff --git a/src/TurboHTTP.IntegrationTests.Client/Shared/FeatureSpecBase.cs b/src/TurboHTTP.IntegrationTests.Client/Shared/FeatureSpecBase.cs index dbc7c5144..bb4fa1bb6 100644 --- a/src/TurboHTTP.IntegrationTests.Client/Shared/FeatureSpecBase.cs +++ b/src/TurboHTTP.IntegrationTests.Client/Shared/FeatureSpecBase.cs @@ -1,5 +1,4 @@ using TurboHTTP.Tests.Shared; -using TurboHTTP.IntegrationTests.Client.Shared; namespace TurboHTTP.IntegrationTests.Client.Shared; diff --git a/src/TurboHTTP.IntegrationTests.Client/Shared/ServerContainerFixture.cs b/src/TurboHTTP.IntegrationTests.Client/Shared/ServerContainerFixture.cs index 8925fa456..7ba72e5dd 100644 --- a/src/TurboHTTP.IntegrationTests.Client/Shared/ServerContainerFixture.cs +++ b/src/TurboHTTP.IntegrationTests.Client/Shared/ServerContainerFixture.cs @@ -1,6 +1,3 @@ -using TurboHTTP.Tests.Shared; -using TurboHTTP.IntegrationTests.Client.Shared; - namespace TurboHTTP.IntegrationTests.Client.Shared; public sealed class ServerContainerFixture : Xunit.IAsyncLifetime diff --git a/src/TurboHTTP.IntegrationTests.End2End/H10/LargePayloadSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H10/LargePayloadSpec.cs new file mode 100644 index 000000000..f9126724c --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H10/LargePayloadSpec.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H10; + +[Collection("H10")] +public sealed class LargePayloadSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version10; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapPost("/echo-bytes", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var data = stream.ToArray(); + ctx.Response.ContentType = "application/octet-stream"; + await ctx.Response.Body.WriteAsync(data, CancellationToken); + }); + + app.MapGet("/generate", async (int size, HttpContext ctx) => + { + ctx.Response.ContentType = "application/octet-stream"; + var buffer = new byte[1024]; + Array.Fill(buffer, (byte)0xAB); + var remaining = size; + while (remaining > 0) + { + var toWrite = Math.Min(1024, remaining); + await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, toWrite), CancellationToken); + remaining -= toWrite; + } + }); + + app.MapPost("/empty-echo", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var length = stream.Length; + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(length.ToString(), CancellationToken); + }); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_roundtrip_body_over_64kb() + { + var payload = new byte[128 * 1024]; + RandomNumberGenerator.Fill(payload); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo-bytes") + { + Content = new ByteArrayContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(payload, responseBytes); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_receive_large_server_response() + { + var size = 256 * 1024; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/generate?size={size}"); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(size, responseBytes.Length); + Assert.True(responseBytes.All(b => b == 0xAB)); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_handle_empty_body() + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/empty-echo") + { + Content = new ByteArrayContent([]) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("0", body); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H10/ResilienceSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H10/ResilienceSpec.cs new file mode 100644 index 000000000..80f4ef52b --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H10/ResilienceSpec.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H10; + +[Collection("H10")] +public sealed class ResilienceSpec : End2EndSpecBase +{ + private readonly TaskCompletionSource _handlerGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override Version ProtocolVersion => HttpVersion.Version10; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/fast", () => Results.Text("ok")); + + app.MapGet("/slow", async () => + { + await Task.Delay(30000, CancellationToken); + return Results.Ok("done"); + }); + + app.MapGet("/blocked", async () => + { + await _handlerGate.Task; + return Results.Ok("unblocked"); + }); + } + + public override async ValueTask DisposeAsync() + { + _handlerGate.TrySetResult(); + await base.DisposeAsync(); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_complete_fast_request() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/fast"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("ok", body); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_timeout_slow_request() + { + Client.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, CancellationToken)); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_cancel_via_cancellation_token() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, cts.Token)); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H10/RoundtripSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H10/RoundtripSpec.cs new file mode 100644 index 000000000..6d970b2ad --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H10/RoundtripSpec.cs @@ -0,0 +1,65 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H10; + +[Collection("H10")] +public sealed class RoundtripSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version10; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/hello", () => Results.Ok("Hello World")); + + app.MapPost("/echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + + app.MapDelete("/delete-me", Results.NoContent); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_200_for_get() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/hello"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + Assert.Equal("Hello World", value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_post_body() + { + var payload = "test payload"; + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_404_for_unknown_route() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/nonexistent"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H11/LargePayloadSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H11/LargePayloadSpec.cs new file mode 100644 index 000000000..f1a2f41c5 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H11/LargePayloadSpec.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H11; + +[Collection("H11")] +public sealed class LargePayloadSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version11; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapPost("/echo-bytes", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var data = stream.ToArray(); + ctx.Response.ContentType = "application/octet-stream"; + await ctx.Response.Body.WriteAsync(data, CancellationToken); + }); + + app.MapGet("/generate", async (int size, HttpContext ctx) => + { + ctx.Response.ContentType = "application/octet-stream"; + var buffer = new byte[1024]; + Array.Fill(buffer, (byte)0xAB); + var remaining = size; + while (remaining > 0) + { + var toWrite = Math.Min(1024, remaining); + await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, toWrite), CancellationToken); + remaining -= toWrite; + } + }); + + app.MapPost("/empty-echo", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var length = stream.Length; + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(length.ToString(), CancellationToken); + }); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_roundtrip_body_over_64kb() + { + var payload = new byte[128 * 1024]; + RandomNumberGenerator.Fill(payload); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo-bytes") + { + Content = new ByteArrayContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(payload, responseBytes); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_receive_large_server_response() + { + var size = 256 * 1024; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/generate?size={size}"); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(size, responseBytes.Length); + Assert.True(responseBytes.All(b => b == 0xAB)); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_handle_empty_body() + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/empty-echo") + { + Content = new ByteArrayContent(Array.Empty()) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("0", body); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H11/PipeliningSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H11/PipeliningSpec.cs new file mode 100644 index 000000000..cdc02daa0 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H11/PipeliningSpec.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H11; + +[Collection("H11")] +public sealed class PipeliningSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version11; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/item/{id}", (int id) => Results.Ok(id)); + } + + [Fact(Timeout = 15000)] + public async Task Pipelining_should_return_correct_responses_for_sequential_requests() + { + var responses = new int[5]; + for (var i = 0; i < 5; i++) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/item/{i}"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + responses[i] = value; + } + + for (var i = 0; i < 5; i++) + { + Assert.Equal(i, responses[i]); + } + } + + [Fact(Timeout = 60000)] + public async Task Pipelining_should_handle_concurrent_requests() + { + var tasks = new Task[5]; + for (var i = 0; i < 5; i++) + { + var id = i; + tasks[i] = Task.Run(async () => + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/item/{id}"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + return value; + }); + } + + var results = await Task.WhenAll(tasks); + + var distinctResults = results.Distinct().ToArray(); + Assert.Equal(5, distinctResults.Length); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H11/ResilienceSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H11/ResilienceSpec.cs new file mode 100644 index 000000000..6c8b749c0 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H11/ResilienceSpec.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H11; + +[Collection("H11")] +public sealed class ResilienceSpec : End2EndSpecBase +{ + private readonly TaskCompletionSource _handlerGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override Version ProtocolVersion => HttpVersion.Version11; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/fast", () => Results.Text("ok")); + + app.MapGet("/slow", async () => + { + await Task.Delay(30000, CancellationToken); + return Results.Ok("done"); + }); + + app.MapGet("/blocked", async () => + { + await _handlerGate.Task; + return Results.Ok("unblocked"); + }); + } + + public override async ValueTask DisposeAsync() + { + _handlerGate.TrySetResult(); + await base.DisposeAsync(); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_complete_fast_request() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/fast"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("ok", body); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_timeout_slow_request() + { + Client.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, CancellationToken)); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_cancel_via_cancellation_token() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, cts.Token)); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H11/RoundtripSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H11/RoundtripSpec.cs new file mode 100644 index 000000000..3c92f994f --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H11/RoundtripSpec.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H11; + +[Collection("H11")] +public sealed class RoundtripSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version11; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/hello", () => Results.Ok("Hello World")); + + app.MapPost("/echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + + app.MapPut("/put-echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + + app.MapDelete("/delete-me", () => Results.NoContent()); + + app.MapGet("/headers", (HttpContext ctx) => + { + var customHeader = ctx.Request.Headers["X-Custom-Header"].ToString(); + var response = new { header = customHeader }; + ctx.Response.Headers["X-Echo-Header"] = customHeader; + return Results.Ok(response); + }); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_200_for_get() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/hello"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + Assert.Equal("Hello World", value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_post_body() + { + var payload = "test payload"; + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_put_body() + { + var payload = "test put payload"; + var request = new HttpRequestMessage(HttpMethod.Put, $"{BaseUri}/put-echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_204_for_delete() + { + var request = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUri}/delete-me"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_404_for_unknown_route() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/nonexistent"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_custom_headers() + { + var headerValue = "custom-test-value"; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/headers"); + request.Headers.Add("X-Custom-Header", headerValue); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.NotEmpty(body); + Assert.True(response.Headers.TryGetValues("X-Echo-Header", out var values)); + Assert.Contains(headerValue, values); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H11/StreamingSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H11/StreamingSpec.cs new file mode 100644 index 000000000..152a94e45 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H11/StreamingSpec.cs @@ -0,0 +1,65 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H11; + +[Collection("H11")] +public sealed class StreamingSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version11; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/stream-chunks", async ctx => + { + for (var i = 0; i < 5; i++) + { + await ctx.Response.WriteAsync($"chunk-{i}\n", CancellationToken); + await ctx.Response.Body.FlushAsync(CancellationToken); + } + }); + + app.MapGet("/sse", async ctx => + { + ctx.Response.ContentType = "text/event-stream"; + for (var i = 0; i < 3; i++) + { + await ctx.Response.WriteAsync($"data: event-{i}\n\n", CancellationToken); + await ctx.Response.Body.FlushAsync(CancellationToken); + } + }); + } + + [Fact(Timeout = 15000)] + public async Task Streaming_should_receive_all_chunks() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/stream-chunks"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Contains("chunk-0", body); + Assert.Contains("chunk-1", body); + Assert.Contains("chunk-2", body); + Assert.Contains("chunk-3", body); + Assert.Contains("chunk-4", body); + } + + [Fact(Timeout = 15000)] + public async Task Streaming_should_receive_sse_events() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/sse"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content.Headers.ContentType?.MediaType == "text/event-stream"); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Contains("event-0", body); + Assert.Contains("event-1", body); + Assert.Contains("event-2", body); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/FlowControlSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/FlowControlSpec.cs new file mode 100644 index 000000000..af521168f --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/FlowControlSpec.cs @@ -0,0 +1,67 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class FlowControlSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapPost("/echo-bytes", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var data = stream.ToArray(); + ctx.Response.ContentType = "application/octet-stream"; + await ctx.Response.Body.WriteAsync(data, CancellationToken); + }); + + app.MapGet("/generate-large", async ctx => + { + ctx.Response.ContentType = "application/octet-stream"; + var buffer = new byte[16 * 1024]; + Array.Fill(buffer, (byte)0xCD); + for (var i = 0; i < 64; i++) + { + await ctx.Response.Body.WriteAsync(buffer, CancellationToken); + } + }); + } + + [Fact(Timeout = 30000)] + public async Task FlowControl_should_transfer_large_body_under_backpressure() + { + var payload = new byte[512 * 1024]; + RandomNumberGenerator.Fill(payload); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo-bytes") + { + Content = new ByteArrayContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(payload, responseBytes); + } + + [Fact(Timeout = 30000)] + public async Task FlowControl_should_receive_large_server_generated_response() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/generate-large"); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + var expectedSize = 64 * 16 * 1024; + Assert.Equal(expectedSize, responseBytes.Length); + Assert.True(responseBytes.All(b => b == 0xCD)); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/LargePayloadSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/LargePayloadSpec.cs new file mode 100644 index 000000000..6b8ae91c1 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/LargePayloadSpec.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class LargePayloadSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapPost("/echo-bytes", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var data = stream.ToArray(); + ctx.Response.ContentType = "application/octet-stream"; + await ctx.Response.Body.WriteAsync(data, CancellationToken); + }); + + app.MapGet("/generate", async (int size, HttpContext ctx) => + { + ctx.Response.ContentType = "application/octet-stream"; + var buffer = new byte[1024]; + Array.Fill(buffer, (byte)0xAB); + var remaining = size; + while (remaining > 0) + { + var toWrite = Math.Min(1024, remaining); + await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, toWrite), CancellationToken); + remaining -= toWrite; + } + }); + + app.MapPost("/empty-echo", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var length = stream.Length; + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(length.ToString(), CancellationToken); + }); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_roundtrip_body_over_64kb() + { + var payload = new byte[128 * 1024]; + RandomNumberGenerator.Fill(payload); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo-bytes") + { + Content = new ByteArrayContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(payload, responseBytes); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_receive_large_server_response() + { + var size = 256 * 1024; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/generate?size={size}"); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(size, responseBytes.Length); + Assert.True(responseBytes.All(b => b == 0xAB)); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_handle_empty_body() + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/empty-echo") + { + Content = new ByteArrayContent(Array.Empty()) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("0", body); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/MultiplexingSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/MultiplexingSpec.cs new file mode 100644 index 000000000..e6760595c --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/MultiplexingSpec.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class MultiplexingSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/id/{id}", (int id) => Results.Ok(id)); + + app.MapGet("/delay/{ms}", async (int ms) => + { + await Task.Delay(ms, CancellationToken); + return Results.Ok(ms); + }); + } + + [Fact(Timeout = 30000)] + public async Task Multiplexing_should_handle_parallel_streams() + { + var tasks = new Task[20]; + for (var i = 0; i < 20; i++) + { + var id = i; + tasks[i] = Task.Run(async () => + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/id/{id}"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + return value; + }); + } + + var results = await Task.WhenAll(tasks); + + var distinctResults = results.Distinct().ToArray(); + Assert.Equal(20, distinctResults.Length); + } + + [Fact(Timeout = 30000)] + public async Task Multiplexing_should_not_starve_fast_streams() + { + var slowTask = Task.Run(async () => + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/delay/2000"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + return JsonSerializer.Deserialize(body); + }); + + await Task.Delay(100, CancellationToken); + + var fastStart = DateTime.UtcNow; + var fastRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/id/42"); + var fastResponse = await Client.SendAsync(fastRequest, CancellationToken); + var fastElapsed = DateTime.UtcNow - fastStart; + + Assert.Equal(HttpStatusCode.OK, fastResponse.StatusCode); + var fastBody = await fastResponse.Content.ReadAsStringAsync(CancellationToken); + var fastValue = JsonSerializer.Deserialize(fastBody); + Assert.Equal(42, fastValue); + + Assert.True(fastElapsed < TimeSpan.FromSeconds(1), $"Fast request took {fastElapsed.TotalMilliseconds}ms"); + + await slowTask; + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/ResilienceSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/ResilienceSpec.cs new file mode 100644 index 000000000..5aed21c2b --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/ResilienceSpec.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class ResilienceSpec : End2EndSpecBase +{ + private readonly TaskCompletionSource _handlerGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/fast", () => Results.Text("ok")); + + app.MapGet("/slow", async () => + { + await Task.Delay(30000, CancellationToken); + return Results.Ok("done"); + }); + + app.MapGet("/blocked", async () => + { + await _handlerGate.Task; + return Results.Ok("unblocked"); + }); + } + + public override async ValueTask DisposeAsync() + { + _handlerGate.TrySetResult(); + await base.DisposeAsync(); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_complete_fast_request() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/fast"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("ok", body); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_timeout_slow_request() + { + Client.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, CancellationToken)); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_cancel_via_cancellation_token() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, cts.Token)); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/RoundtripSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/RoundtripSpec.cs new file mode 100644 index 000000000..3bbb3b72b --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/RoundtripSpec.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class RoundtripSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/hello", () => Results.Ok("Hello World")); + + app.MapPost("/echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + + app.MapPut("/put-echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + + app.MapDelete("/delete-me", () => Results.NoContent()); + + app.MapGet("/headers", (HttpContext ctx) => + { + var customHeader = ctx.Request.Headers["X-Custom-Header"].ToString(); + var response = new { header = customHeader }; + ctx.Response.Headers["X-Echo-Header"] = customHeader; + return Results.Ok(response); + }); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_200_for_get() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/hello"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + Assert.Equal("Hello World", value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_post_body() + { + var payload = "test payload"; + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_put_body() + { + var payload = "test put payload"; + var request = new HttpRequestMessage(HttpMethod.Put, $"{BaseUri}/put-echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_204_for_delete() + { + var request = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUri}/delete-me"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_404_for_unknown_route() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/nonexistent"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_custom_headers() + { + var headerValue = "custom-test-value"; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/headers"); + request.Headers.Add("X-Custom-Header", headerValue); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.NotEmpty(body); + Assert.True(response.Headers.TryGetValues("X-Echo-Header", out var values)); + Assert.Contains(headerValue, values); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H2/UpgradeSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H2/UpgradeSpec.cs new file mode 100644 index 000000000..2d787b796 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H2/UpgradeSpec.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Servus.Akka.Transport; +using TurboHTTP.Server; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H2; + +[Collection("H2")] +public sealed class UpgradeSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version20; + + protected override bool UseTls => false; + + protected override void ConfigureServer(TurboServerOptions options, ushort port, System.Security.Cryptography.X509Certificates.X509Certificate2? cert) + { + options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); + } + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/hello", () => Results.Ok("Hello via h2c")); + } + + [Fact(Timeout = 15000)] + public async Task Upgrade_should_communicate_via_h2c() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/hello"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + Assert.Equal("Hello via h2c", value); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H3/LargePayloadSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H3/LargePayloadSpec.cs new file mode 100644 index 000000000..5b10c81ad --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H3/LargePayloadSpec.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H3; + +[Collection("H3")] +public sealed class LargePayloadSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version30; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapPost("/echo-bytes", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var data = stream.ToArray(); + ctx.Response.ContentType = "application/octet-stream"; + await ctx.Response.Body.WriteAsync(data, CancellationToken); + }); + + app.MapGet("/generate", async (int size, HttpContext ctx) => + { + ctx.Response.ContentType = "application/octet-stream"; + var buffer = new byte[1024]; + Array.Fill(buffer, (byte)0xAB); + var remaining = size; + while (remaining > 0) + { + var toWrite = Math.Min(1024, remaining); + await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, toWrite), CancellationToken); + remaining -= toWrite; + } + }); + + app.MapPost("/empty-echo", async ctx => + { + using var stream = new MemoryStream(); + await ctx.Request.Body.CopyToAsync(stream, CancellationToken); + var length = stream.Length; + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(length.ToString(), CancellationToken); + }); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_roundtrip_body_over_64kb() + { + var payload = new byte[128 * 1024]; + RandomNumberGenerator.Fill(payload); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo-bytes") + { + Content = new ByteArrayContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(payload, responseBytes); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_receive_large_server_response() + { + var size = 256 * 1024; + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/generate?size={size}"); + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseBytes = await response.Content.ReadAsByteArrayAsync(CancellationToken); + Assert.Equal(size, responseBytes.Length); + Assert.True(responseBytes.All(b => b == 0xAB)); + } + + [Fact(Timeout = 30000)] + public async Task LargePayload_should_handle_empty_body() + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/empty-echo") + { + Content = new ByteArrayContent(Array.Empty()) + }; + + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("0", body); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H3/MultiplexingSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H3/MultiplexingSpec.cs new file mode 100644 index 000000000..ead05afb5 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H3/MultiplexingSpec.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H3; + +[Collection("H3")] +public sealed class MultiplexingSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version30; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/id/{id:int}", (int id) => Results.Ok(id)); + + app.MapGet("/delay/{ms:int}", async (int ms) => + { + await Task.Delay(ms, CancellationToken); + return Results.Ok(ms); + }); + } + + [Fact(Timeout = 30000)] + public async Task Multiplexing_should_handle_parallel_streams() + { + var tasks = new Task[20]; + for (var i = 0; i < 20; i++) + { + var id = i; + tasks[i] = Task.Run(async () => + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/id/{id}"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + return value; + }); + } + + var results = await Task.WhenAll(tasks); + + var distinctResults = results.Distinct().ToArray(); + Assert.Equal(20, distinctResults.Length); + } + + [Fact(Timeout = 30000)] + public async Task Multiplexing_should_not_starve_fast_streams() + { + var slowTask = Task.Run(async () => + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/delay/2000"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + return JsonSerializer.Deserialize(body); + }); + + await Task.Delay(100, CancellationToken); + + var fastStart = DateTime.UtcNow; + var fastRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/id/42"); + var fastResponse = await Client.SendAsync(fastRequest, CancellationToken); + var fastElapsed = DateTime.UtcNow - fastStart; + + Assert.Equal(HttpStatusCode.OK, fastResponse.StatusCode); + var fastBody = await fastResponse.Content.ReadAsStringAsync(CancellationToken); + var fastValue = JsonSerializer.Deserialize(fastBody); + Assert.Equal(42, fastValue); + + Assert.True(fastElapsed < TimeSpan.FromSeconds(1), $"Fast request took {fastElapsed.TotalMilliseconds}ms"); + + await slowTask; + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H3/ResilienceSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H3/ResilienceSpec.cs new file mode 100644 index 000000000..615efad05 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H3/ResilienceSpec.cs @@ -0,0 +1,70 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H3; + +[Collection("H3")] +public sealed class ResilienceSpec : End2EndSpecBase +{ + private readonly TaskCompletionSource _handlerGate = new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override Version ProtocolVersion => HttpVersion.Version30; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/fast", () => Results.Text("ok")); + + app.MapGet("/slow", async () => + { + await Task.Delay(30000, CancellationToken); + return Results.Ok("done"); + }); + + app.MapGet("/blocked", async () => + { + await _handlerGate.Task; + return Results.Ok("unblocked"); + }); + } + + public override async ValueTask DisposeAsync() + { + _handlerGate.TrySetResult(); + await base.DisposeAsync(); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_complete_fast_request() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/fast"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + Assert.Equal("ok", body); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_timeout_slow_request() + { + Client.Timeout = TimeSpan.FromSeconds(2); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, CancellationToken)); + } + + [Fact(Timeout = 15000)] + public async Task Resilience_should_cancel_via_cancellation_token() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); + + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/slow"); + + await Assert.ThrowsAnyAsync( + async () => await Client.SendAsync(request, cts.Token)); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/H3/RoundtripSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/H3/RoundtripSpec.cs new file mode 100644 index 000000000..34e0854f5 --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/H3/RoundtripSpec.cs @@ -0,0 +1,63 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using TurboHTTP.IntegrationTests.End2End.Shared; + +namespace TurboHTTP.IntegrationTests.End2End.H3; + +[Collection("H3")] +public sealed class RoundtripSpec : End2EndSpecBase +{ + protected override Version ProtocolVersion => HttpVersion.Version30; + + protected override void ConfigureEndpoints(WebApplication app) + { + app.MapGet("/hello", () => Results.Ok("Hello World")); + + app.MapPost("/echo", async (HttpContext ctx) => + { + using var reader = new StreamReader(ctx.Request.Body); + var body = await reader.ReadToEndAsync(CancellationToken); + return Results.Ok(body); + }); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_200_for_get() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/hello"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + var value = JsonSerializer.Deserialize(body); + Assert.Equal("Hello World", value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_echo_post_body() + { + var payload = "test payload"; + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUri}/echo") + { + Content = new StringContent(payload) + }; + + var response = await Client.SendAsync(request, CancellationToken); + var body = await response.Content.ReadAsStringAsync(CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = JsonSerializer.Deserialize(body); + Assert.Equal(payload, value); + } + + [Fact(Timeout = 15000)] + public async Task Roundtrip_should_return_404_for_unknown_route() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/nonexistent"); + var response = await Client.SendAsync(request, CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/Shared/Collections.cs b/src/TurboHTTP.IntegrationTests.End2End/Shared/Collections.cs new file mode 100644 index 000000000..ff0f1823f --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/Shared/Collections.cs @@ -0,0 +1,13 @@ +namespace TurboHTTP.IntegrationTests.End2End.Shared; + +[CollectionDefinition("H10")] +public sealed class H10End2EndCollection; + +[CollectionDefinition("H11")] +public sealed class H11End2EndCollection; + +[CollectionDefinition("H2")] +public sealed class H2End2EndCollection; + +[CollectionDefinition("H3")] +public sealed class H3End2EndCollection; diff --git a/src/TurboHTTP.IntegrationTests.End2End/Shared/End2EndSpecBase.cs b/src/TurboHTTP.IntegrationTests.End2End/Shared/End2EndSpecBase.cs new file mode 100644 index 000000000..d645d481d --- /dev/null +++ b/src/TurboHTTP.IntegrationTests.End2End/Shared/End2EndSpecBase.cs @@ -0,0 +1,204 @@ +using System.Net; +using System.Net.Quic; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Akka.Actor; +using Akka.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Servus.Akka.Transport; +using TurboHTTP.Client; +using TurboHTTP.Server; +using QuicListenerOptionsServus = Servus.Akka.Transport.QuicListenerOptions; + +namespace TurboHTTP.IntegrationTests.End2End.Shared; + +public abstract class End2EndSpecBase : IAsyncLifetime +{ + private WebApplication? _app; + private ITurboHttpClient? _client; + private Microsoft.Extensions.DependencyInjection.ServiceProvider? _clientProvider; + private X509Certificate2? _cert; + + protected abstract Version ProtocolVersion { get; } + + protected abstract void ConfigureEndpoints(WebApplication app); + + protected virtual bool UseTls => ProtocolVersion.Major >= 2; + + protected virtual void ConfigureServer(TurboServerOptions options, ushort port, X509Certificate2? cert) + { + if (ProtocolVersion.Major == 3) + { + if (!QuicConnection.IsSupported) + { + return; + } + + var quicOptions = new QuicListenerOptionsServus + { + Host = "127.0.0.1", + Port = port, + ServerCertificate = cert!, + ApplicationProtocols = new List { SslApplicationProtocol.Http3 } + }; + + options.Bind(quicOptions); + } + else if (ProtocolVersion.Major == 2) + { + options.ListenLocalhost(port, listen => + { + listen.UseHttps(cert!); + listen.Protocols = HttpProtocols.Http2; + }); + } + else + { + options.Bind(new TcpListenerOptions + { + Host = "127.0.0.1", + Port = port + }); + } + } + + protected virtual void ConfigureClientOptions(TurboClientOptions options) + { + } + + protected ITurboHttpClient Client => _client!; + + protected string BaseUri { get; private set; } = string.Empty; + + protected CancellationToken CancellationToken => TestContext.Current.CancellationToken; + + public async ValueTask InitializeAsync() + { + if (ProtocolVersion.Major == 3 && !QuicConnection.IsSupported) + { + Assert.Skip("QUIC not available on this platform"); + } + + var port = GetFreePort(); + var needsTls = UseTls; + + if (needsTls) + { + _cert = CreateSelfSignedCertificate("127.0.0.1"); + } + + var scheme = needsTls ? "https" : "http"; + BaseUri = $"{scheme}://127.0.0.1:{port}"; + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + builder.Host.UseTurboHttp(options => + { + ConfigureServer(options, port, _cert); + }); + + _app = builder.Build(); + ConfigureEndpoints(_app); + await _app.StartAsync(); + + var services = new ServiceCollection(); + + var diSetup = DependencyResolverSetup.Create(services.BuildServiceProvider()); + var bootstrap = BootstrapSetup.Create(); + var system = ActorSystem.Create($"e2e-client-{Guid.NewGuid():N}", bootstrap.And(diSetup)); + services.AddSingleton(system); + + var clientOptions = new TurboClientOptions + { + BaseAddress = new Uri(BaseUri), + DangerousAcceptAnyServerCertificate = needsTls + }; + + ConfigureClientOptions(clientOptions); + + services.AddTurboHttpClient(); + services.Replace(ServiceDescriptor.Singleton>( + new FixedOptionsFactory(clientOptions))); + + _clientProvider = services.BuildServiceProvider(); + + var factory = _clientProvider.GetRequiredService(); + _client = factory.CreateClient(string.Empty); + _client.DefaultRequestVersion = ProtocolVersion; + _client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + _client.Timeout = TimeSpan.FromSeconds(10); + } + + public virtual async ValueTask DisposeAsync() + { + _client?.Dispose(); + + if (_app is not null) + { + await _app.StopAsync(); + await _app.DisposeAsync(); + } + + if (_clientProvider is not null) + { + var system = _clientProvider.GetService(); + if (system is not null) + { + await system.Terminate().WaitAsync(TimeSpan.FromSeconds(10)); + await system.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(5)); + } + + await _clientProvider.DisposeAsync(); + } + + _cert?.Dispose(); + } + + protected static X509Certificate2 CreateSelfSignedCertificate(string cn) + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + $"CN={cn}", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + request.CertificateExtensions.Add( + new X509BasicConstraintsExtension(false, false, 0, false)); + + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(cn); + sanBuilder.AddIpAddress(IPAddress.Parse("127.0.0.1")); + request.CertificateExtensions.Add(sanBuilder.Build()); + + var cert = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-1), + DateTimeOffset.UtcNow.AddDays(1)); + + return X509CertificateLoader.LoadPkcs12( + cert.Export(X509ContentType.Pfx), + null, + X509KeyStorageFlags.Exportable); + } + + private static ushort GetFreePort() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return (ushort)port; + } + + private sealed class FixedOptionsFactory(TurboClientOptions options) : IOptionsFactory + { + public TurboClientOptions Create(string name) => options; + } +} diff --git a/src/TurboHTTP.IntegrationTests.End2End/SseEndToEndSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/SseEndToEndSpec.cs deleted file mode 100644 index 81808a761..000000000 --- a/src/TurboHTTP.IntegrationTests.End2End/SseEndToEndSpec.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Net; -using Akka.Actor; -using Akka.Streams; -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http; -using TurboHTTP.Client; -using TurboHTTP.Features.Sse; -using TurboHTTP.Tests.Shared; -using TurboHTTP.Server; - -namespace TurboHTTP.IntegrationTests.End2End; - -public sealed class SseEndToEndSpec : IAsyncLifetime -{ - private TurboServerFixture? _fixture; - private ClientHelper? _client; - private ActorSystem? _system; - private IMaterializer? _materializer; - - public async ValueTask InitializeAsync() - { - _fixture = new TurboServerFixture(app => - { - app.MapGet("/events", () => - { - var source = Source.From(["hello", "world"]) - .Select(msg => msg); - return TurboStreamResults.EventStream(source); - }); - - app.MapGet("/echo", () => Results.Ok("ok")); - }); - - await _fixture.InitializeAsync(); - - _system = ActorSystem.Create("e2e-test"); - _materializer = _system.Materializer(); - - _client = ClientHelper.CreateClient( - _fixture.HttpPort, - new Version(1, 1), - system: _system); - } - - public async ValueTask DisposeAsync() - { - if (_client is not null) - { - await _client.DisposeAsync(); - } - - if (_system is not null) - { - await _system.Terminate(); - } - - if (_fixture is not null) - { - await _fixture.DisposeAsync(); - } - } - - [Fact(Timeout = 15000)] - public async Task TurboClient_should_receive_response_from_turbo_server() - { - var request = new HttpRequestMessage(HttpMethod.Get, "/echo"); - var response = await _client!.Client.SendAsync(request, TestContext.Current.CancellationToken); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact(Timeout = 15000)] - public async Task TurboClient_should_consume_sse_from_turbo_server() - { - var request = new HttpRequestMessage(HttpMethod.Get, "/events"); - var response = await _client!.Client.SendAsync(request, TestContext.Current.CancellationToken); - - var events = await response.AsEventStream() - .RunWith(Sink.Seq(), _materializer!); - - Assert.Equal(2, events.Count); - Assert.Equal("hello", events[0].Data); - Assert.Equal("world", events[1].Data); - } -} diff --git a/src/TurboHTTP.IntegrationTests.End2End/TrailerEndToEndSpec.cs b/src/TurboHTTP.IntegrationTests.End2End/TrailerEndToEndSpec.cs deleted file mode 100644 index b806c0826..000000000 --- a/src/TurboHTTP.IntegrationTests.End2End/TrailerEndToEndSpec.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Net; -using Akka.Actor; -using Microsoft.AspNetCore.Http; -using TurboHTTP.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.IntegrationTests.End2End; - -public sealed class TrailerEndToEndSpec : IAsyncLifetime -{ - private TurboServerFixture? _fixture; - private ClientHelper? _client; - private ActorSystem? _system; - - public async ValueTask InitializeAsync() - { - _fixture = new TurboServerFixture(app => - { - app.MapPost("/echo-with-trailers", (TurboHttpContext ctx) => - { - ctx.TurboResponse.AppendTrailer("grpc-status", "0"); - ctx.TurboResponse.AppendTrailer("grpc-message", "OK"); - return Results.Ok("response body"); - }); - - app.MapPost("/echo-with-prohibited-trailers", (TurboHttpContext ctx) => - { - ctx.TurboResponse.AppendTrailer("grpc-status", "0"); - ctx.TurboResponse.AppendTrailer("content-length", "13"); - ctx.TurboResponse.AppendTrailer("transfer-encoding", "chunked"); - return Results.Ok("response body"); - }); - }); - - await _fixture.InitializeAsync(); - - _system = ActorSystem.Create("trailer-e2e-test"); - - _client = ClientHelper.CreateClient( - _fixture.HttpPort, - new Version(1, 1), - system: _system); - } - - [Fact(Timeout = 15000)] - public async Task Client_should_receive_basic_response() - { - var request = new HttpRequestMessage(HttpMethod.Post, "/echo-with-trailers") - { - Content = new StringContent("request body") - }; - - var response = await _client!.Client.SendAsync(request, TestContext.Current.CancellationToken); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - Assert.Equal("\"response body\"", body); - } - - public async ValueTask DisposeAsync() - { - if (_client is not null) - { - await _client.DisposeAsync(); - } - - if (_system is not null) - { - await _system.Terminate(); - } - - if (_fixture is not null) - { - await _fixture.DisposeAsync(); - } - } - - [Fact(Timeout = 15000)] - [Trait("RFC", "RFC9113-8.1")] - public async Task Client_should_receive_trailers_from_server() - { - var request = new HttpRequestMessage(HttpMethod.Post, "/echo-with-trailers") - { - Content = new StringContent("request body") - }; - - var response = await _client!.Client.SendAsync(request, TestContext.Current.CancellationToken); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - Assert.Equal("\"response body\"", body); - - // For HTTP/1.1 with chunked encoding, trailers are in TrailingHeaders if available - if (response.TrailingHeaders.Any()) - { - Assert.Equal("0", response.TrailingHeaders.GetValues("grpc-status").FirstOrDefault()); - Assert.Equal("OK", response.TrailingHeaders.GetValues("grpc-message").FirstOrDefault()); - } - } - - [Fact(Timeout = 15000)] - [Trait("RFC", "RFC9110-6.5.1")] - public async Task Client_should_not_receive_prohibited_trailer_fields() - { - var request = new HttpRequestMessage(HttpMethod.Post, "/echo-with-prohibited-trailers") - { - Content = new StringContent("request body") - }; - - var response = await _client!.Client.SendAsync(request, TestContext.Current.CancellationToken); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.TrailingHeaders); - Assert.DoesNotContain(response.TrailingHeaders, h => - h.Key.Equals("content-length", StringComparison.OrdinalIgnoreCase) || - h.Key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase)); - } -} \ No newline at end of file diff --git a/src/TurboHTTP.IntegrationTests.End2End/TurboHTTP.IntegrationTests.End2End.csproj b/src/TurboHTTP.IntegrationTests.End2End/TurboHTTP.IntegrationTests.End2End.csproj index 173f8c1c9..28f4fa6da 100644 --- a/src/TurboHTTP.IntegrationTests.End2End/TurboHTTP.IntegrationTests.End2End.csproj +++ b/src/TurboHTTP.IntegrationTests.End2End/TurboHTTP.IntegrationTests.End2End.csproj @@ -22,6 +22,7 @@ + diff --git a/src/TurboHTTP.IntegrationTests.End2End/xunit.runner.json b/src/TurboHTTP.IntegrationTests.End2End/xunit.runner.json index 08c512b3d..4c6a0fdf5 100644 --- a/src/TurboHTTP.IntegrationTests.End2End/xunit.runner.json +++ b/src/TurboHTTP.IntegrationTests.End2End/xunit.runner.json @@ -1,4 +1,4 @@ { "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "parallelizeTestCollections": false + "parallelizeTestCollections": true } diff --git a/src/TurboHTTP.IntegrationTests.Server/Hosting/HttpsConnectionSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Hosting/HttpsConnectionSpec.cs index f7daa8909..371770144 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Hosting/HttpsConnectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Hosting/HttpsConnectionSpec.cs @@ -1,19 +1,18 @@ using System.Net; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Hosting; public sealed class HttpsConnectionSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { var certificate = CreateSelfSignedCertificate("localhost"); - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.ListenLocalhost(port, listen => { @@ -23,9 +22,9 @@ protected override void ConfigureServer(IServiceCollection services, ushort port }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/secure-hello", () => Results.Ok("Hello from HTTPS")); + app.MapGet("/secure-hello", () => Results.Ok("Hello from HTTPS")); } protected override HttpClient CreateHttpClient() => CreateTlsClient(); diff --git a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeAllowSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeAllowSpec.cs index e54eba02a..bd804f21b 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeAllowSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeAllowSpec.cs @@ -1,10 +1,9 @@ using System.Net; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Hosting.Tls; @@ -14,12 +13,12 @@ public sealed class ClientCertificateModeAllowSpec : ServerSpecBase private X509Certificate2? _serverCert; private X509Certificate2? _clientCert; - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { _serverCert = CreateSelfSignedCertificate("localhost"); _clientCert = CreateSelfSignedCertificate("client"); - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.ListenLocalhost(port, listen => { @@ -33,9 +32,9 @@ protected override void ConfigureServer(IServiceCollection services, ushort port }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/test", () => Results.Ok("OK")); + app.MapGet("/test", () => Results.Ok("OK")); } protected override HttpClient? CreateHttpClient() => null; @@ -70,4 +69,4 @@ public async Task AllowCertificate_should_accept_request_with_client_certificate Assert.Equal(HttpStatusCode.OK, response.StatusCode); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeRequireSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeRequireSpec.cs index d52b10645..433c1893f 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeRequireSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/ClientCertificateModeRequireSpec.cs @@ -1,10 +1,9 @@ using System.Net; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Hosting.Tls; @@ -14,12 +13,12 @@ public sealed class ClientCertificateModeRequireSpec : ServerSpecBase private X509Certificate2? _serverCert; private X509Certificate2? _clientCert; - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { _serverCert = CreateSelfSignedCertificate("localhost"); _clientCert = CreateSelfSignedCertificate("client"); - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.ListenLocalhost(port, listen => { @@ -33,9 +32,9 @@ protected override void ConfigureServer(IServiceCollection services, ushort port }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/test", () => Results.Ok("OK")); + app.MapGet("/test", () => Results.Ok("OK")); } protected override HttpClient? CreateHttpClient() => null; @@ -71,4 +70,4 @@ public async Task RequireCertificate_should_accept_request_with_valid_client_cer Assert.Equal(HttpStatusCode.OK, response.StatusCode); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/SniCertSelectionSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/SniCertSelectionSpec.cs index 902bd550a..be5aa39e4 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/SniCertSelectionSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/SniCertSelectionSpec.cs @@ -1,9 +1,8 @@ using System.Net; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Hosting.Tls; @@ -14,13 +13,13 @@ public sealed class SniCertSelectionSpec : ServerSpecBase private X509Certificate2? _certB; private int _selectorCallCount; - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { _certA = CreateSelfSignedCertificate("host-a.local"); _certB = CreateSelfSignedCertificate("host-b.local"); _selectorCallCount = 0; - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.ListenLocalhost(port, listen => { @@ -37,9 +36,9 @@ protected override void ConfigureServer(IServiceCollection services, ushort port }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/sni-test", () => Results.Ok("SNI test response")); + app.MapGet("/sni-test", () => Results.Ok("SNI test response")); } protected override HttpClient CreateHttpClient() => CreateTlsClient(); @@ -73,4 +72,4 @@ public async Task ServerCertificateSelector_should_be_invoked_for_handshake() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(_selectorCallCount > initialCount, "Selector should have been called"); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/TlsHandshakeFeatureSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/TlsHandshakeFeatureSpec.cs index 9f0f2fb71..d8ed79dab 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/TlsHandshakeFeatureSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Hosting/Tls/TlsHandshakeFeatureSpec.cs @@ -1,20 +1,19 @@ using System.Net; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Context.Features; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.IntegrationTests.Server.Hosting.Tls; public sealed class TlsHandshakeFeatureSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { var certificate = CreateSelfSignedCertificate("localhost"); - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.ListenLocalhost(port, listen => { @@ -24,9 +23,9 @@ protected override void ConfigureServer(IServiceCollection services, ushort port }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/tls-info", (HttpContext context) => + app.MapGet("/tls-info", (HttpContext context) => { var tls = context.Features.Get(); if (tls is null) diff --git a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/ConnectionLimitSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/ConnectionLimitSpec.cs index 3fde4abff..83b79e4be 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/ConnectionLimitSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/ConnectionLimitSpec.cs @@ -1,9 +1,8 @@ using System.Net; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Infrastructure; @@ -13,30 +12,30 @@ public sealed class ConnectionLimitSpec : ServerSpecBase private readonly TaskCompletionSource _slot1Gate = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource _slot2Gate = new(TaskCreationOptions.RunContinuationsAsynchronously); - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); - options.MaxConcurrentConnections = 2; + options.Limits.MaxConcurrentConnections = 2; }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/block-slot-1", async () => + app.MapGet("/block-slot-1", async () => { await _slot1Gate.Task; return Results.Ok("slot1-done"); }); - routeTable.Add("GET", "/block-slot-2", async () => + app.MapGet("/block-slot-2", async () => { await _slot2Gate.Task; return Results.Ok("slot2-done"); }); - routeTable.Add("GET", "/fast", () => Results.Ok("ok")); + app.MapGet("/fast", () => Results.Ok("ok")); } public override async ValueTask DisposeAsync() @@ -57,24 +56,20 @@ public async Task Server_should_accept_connections_within_limit() [Fact(Timeout = 20000)] public async Task Server_should_reject_connections_beyond_limit() { - // Fill up 2 connection slots with blocking handlers var client1 = new HttpClient(); var client2 = new HttpClient(); var client3 = new HttpClient(); - // Start first request to occupy slot 1 var request1 = client1.GetAsync( new Uri($"http://127.0.0.1:{Port}/block-slot-1"), CancellationToken); await Task.Delay(200, CancellationToken); - // Start second request to occupy slot 2 var request2 = client2.GetAsync( new Uri($"http://127.0.0.1:{Port}/block-slot-2"), CancellationToken); await Task.Delay(200, CancellationToken); - // Third request should be rejected or timeout (no slots available) bool request3Failed; try { @@ -82,21 +77,17 @@ public async Task Server_should_reject_connections_beyond_limit() var response = await client3.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), cts.Token); - // If we got a response, check if it's an error request3Failed = !response.IsSuccessStatusCode; } catch (OperationCanceledException) { - // Timed out = effectively rejected request3Failed = true; } catch (HttpRequestException) { - // Connection refused = rejected request3Failed = true; } - // Clean up _slot1Gate.TrySetResult(); _slot2Gate.TrySetResult(); @@ -116,14 +107,12 @@ public async Task Server_should_reject_connections_beyond_limit() [Fact(Timeout = 20000)] public async Task Server_should_accept_after_connection_closes() { - // Use first connection (within limit) var response = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Server should still accept subsequent connections response = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/GracefulShutdownSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/GracefulShutdownSpec.cs index 6c86ee295..2c3c443b4 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/GracefulShutdownSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/GracefulShutdownSpec.cs @@ -1,9 +1,8 @@ using System.Net; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Infrastructure; @@ -12,24 +11,24 @@ public sealed class GracefulShutdownSpec : ServerSpecBase { private readonly TaskCompletionSource _handlerGate = new(TaskCreationOptions.RunContinuationsAsynchronously); - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); options.GracefulShutdownTimeout = TimeSpan.FromSeconds(5); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/slow", async () => + app.MapGet("/slow", async () => { await _handlerGate.Task; return Results.Ok("done"); }); - routeTable.Add("GET", "/fast", () => Results.Ok("ok")); + app.MapGet("/fast", () => Results.Ok("ok")); } public override async ValueTask DisposeAsync() @@ -44,16 +43,13 @@ public async Task Shutdown_should_complete_inflight_request() var handlerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var handlerRelease = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Start a slow request that blocks on handlerRelease using var testClient = new HttpClient(); var request = testClient.GetAsync( new Uri($"http://127.0.0.1:{Port}/slow"), TestContext.Current.CancellationToken); - // Wait a bit for server to be ready await Task.Delay(100, TestContext.Current.CancellationToken); - // Verify server is responding var healthCheck = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, healthCheck.StatusCode); @@ -62,7 +58,6 @@ public async Task Shutdown_should_complete_inflight_request() [Fact(Timeout = 20000)] public async Task Shutdown_should_reject_new_connections() { - // Basic sanity check that server is working var response = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/TimeoutSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/TimeoutSpec.cs index 01fae6112..20e7bc037 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Infrastructure/TimeoutSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Infrastructure/TimeoutSpec.cs @@ -1,46 +1,42 @@ using System.Net; using System.Net.Sockets; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Infrastructure; public sealed class TimeoutSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); - options.KeepAliveTimeout = TimeSpan.FromSeconds(2); - options.RequestHeadersTimeout = TimeSpan.FromSeconds(2); + options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(2); + options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(2); options.Http1.KeepAliveTimeout = TimeSpan.FromSeconds(2); options.Http1.RequestHeadersTimeout = TimeSpan.FromSeconds(2); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/fast", () => Results.Ok("ok")); + app.MapGet("/fast", () => Results.Ok("ok")); } [Fact(Timeout = 20000)] public async Task KeepAlive_should_close_idle_connection_after_timeout() { - // First request succeeds var response = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Wait past keep-alive timeout (2s configured, wait 3s) await Task.Delay(TimeSpan.FromSeconds(3), CancellationToken); - // New request with fresh client should still work (server is alive, old connection timed out) using var freshClient = new HttpClient(); response = await freshClient.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); @@ -50,17 +46,14 @@ public async Task KeepAlive_should_close_idle_connection_after_timeout() [Fact(Timeout = 20000)] public async Task RequestHeaders_should_timeout_on_incomplete_headers() { - // Send only partial headers (no final \r\n\r\n), then wait using var tcp = new TcpClient(); await tcp.ConnectAsync(IPAddress.Loopback, Port, CancellationToken); var stream = tcp.GetStream(); var partialBytes = Encoding.ASCII.GetBytes("GET /fast HTTP/1.1\r\nHost: localhost\r\n"); await stream.WriteAsync(partialBytes, CancellationToken); - // Wait past request headers timeout await Task.Delay(TimeSpan.FromSeconds(3), CancellationToken); - // Try to read: connection should be closed (0 bytes) or we get an exception var buffer = new byte[1]; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); try @@ -70,7 +63,6 @@ public async Task RequestHeaders_should_timeout_on_incomplete_headers() } catch (OperationCanceledException) { - // Connection closed, can't read = timeout worked Assert.True(true); } } @@ -78,18 +70,15 @@ public async Task RequestHeaders_should_timeout_on_incomplete_headers() [Fact(Timeout = 20000)] public async Task Server_should_still_respond_after_timeout_disconnects() { - // Cause a timeout disconnect with incomplete headers using (var tcp = new TcpClient()) { await tcp.ConnectAsync(IPAddress.Loopback, Port, CancellationToken); var tcpStream = tcp.GetStream(); var incompleteBytes = Encoding.ASCII.GetBytes("GET /fast HTTP/1.1\r\nHost: localhost\r\n"); await tcpStream.WriteAsync(incompleteBytes, CancellationToken); - // Connection stays open briefly, then times out await Task.Delay(TimeSpan.FromSeconds(3), CancellationToken); } - // Server should still accept new requests var response = await Client.GetAsync( new Uri($"http://127.0.0.1:{Port}/fast"), CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/src/TurboHTTP.IntegrationTests.Server/Lifecycle/ServerSmokeSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Lifecycle/ServerSmokeSpec.cs index aee2650f6..9893217f5 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Lifecycle/ServerSmokeSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Lifecycle/ServerSmokeSpec.cs @@ -1,34 +1,33 @@ using System.Net; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Lifecycle; public sealed class ServerSmokeSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/hello", () => Results.Ok("Hello from TurboHTTP Server")); - routeTable.Add("POST", "/echo", async (HttpContext ctx) => + app.MapGet("/hello", () => Results.Ok("Hello from TurboHTTP Server")); + app.MapPost("/echo", async (HttpContext ctx) => { using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(CancellationToken); return Results.Ok(body); }); - routeTable.Add("GET", "/connection-info", (HttpContext ctx) => + app.MapGet("/connection-info", (HttpContext ctx) => { var remoteIp = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return Results.Ok(remoteIp); diff --git a/src/TurboHTTP.IntegrationTests.Server/Middleware/MiddlewareSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Middleware/MiddlewareSpec.cs index 3f01c19e1..d33560ea2 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Middleware/MiddlewareSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Middleware/MiddlewareSpec.cs @@ -1,46 +1,47 @@ using System.Net; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; namespace TurboHTTP.IntegrationTests.Server.Middleware; public sealed class MiddlewareSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - var pipeline = Services.GetRequiredService(); - - pipeline.Use(async (ctx, next) => + app.Use(async (ctx, next) => { - ctx.Response.Headers["X-Powered-By"] = "TurboHTTP"; + ctx.Response.Headers.XPoweredBy = "TurboHTTP"; await next(ctx); }); - pipeline.Map("/api", api => + app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api => { api.Use(async (ctx, next) => { ctx.Response.Headers["X-Api-Version"] = "2.0"; await next(ctx); }); + api.UseRouting(); + api.UseEndpoints(endpoints => + { + endpoints.MapGet("/api/data", () => Results.Ok(new { value = 42 })); + }); }); - routeTable.Add("GET", "/hello", () => Results.Ok("hello")); - routeTable.Add("GET", "/api/data", () => Results.Ok(new { value = 42 })); - routeTable.Add("GET", "/other", () => Results.Ok("other")); + app.MapGet("/hello", () => Results.Ok("hello")); + app.MapGet("/api/data", () => Results.Ok(new { value = 42 })); + app.MapGet("/other", () => Results.Ok("other")); } [Fact(Timeout = 15000)] diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/ConnectionInfoSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/ConnectionInfoSpec.cs index ba1846256..7d2c61de3 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/ConnectionInfoSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/ConnectionInfoSpec.cs @@ -1,41 +1,34 @@ using System.Net; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class ConnectionInfoSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/connection", (HttpContext ctx) => + app.MapGet("/connection", (HttpContext ctx) => Results.Ok(new { - return Results.Ok(new - { - remoteIp = ctx.Connection.RemoteIpAddress?.ToString(), - remotePort = ctx.Connection.RemotePort, - localIp = ctx.Connection.LocalIpAddress?.ToString(), - localPort = ctx.Connection.LocalPort - }); - }); + remoteIp = ctx.Connection.RemoteIpAddress?.ToString(), + remotePort = ctx.Connection.RemotePort, + localIp = ctx.Connection.LocalIpAddress?.ToString(), + localPort = ctx.Connection.LocalPort + })); - routeTable.Add("GET", "/protocol", (HttpContext ctx) => - { - return Results.Ok(new { protocol = ctx.Request.Protocol }); - }); + app.MapGet("/protocol", (HttpContext ctx) => Results.Ok(new { protocol = ctx.Request.Protocol })); } [Fact(Timeout = 15000)] diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/ErrorHandlingSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/ErrorHandlingSpec.cs index 3caf4a823..2bcd80916 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/ErrorHandlingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/ErrorHandlingSpec.cs @@ -1,26 +1,25 @@ using System.Net; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class ErrorHandlingSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/throw-sync", () => + app.MapGet("/throw-sync", () => { throw new InvalidOperationException("sync boom"); #pragma warning disable CS0162 @@ -28,7 +27,7 @@ protected override void ConfigureRoutes(TurboRouteTable routeTable) #pragma warning restore CS0162 }); - routeTable.Add("GET", "/throw-async", async () => + app.MapGet("/throw-async", async () => { await Task.Yield(); throw new InvalidOperationException("async boom"); @@ -37,7 +36,7 @@ protected override void ConfigureRoutes(TurboRouteTable routeTable) #pragma warning restore CS0162 }); - routeTable.Add("GET", "/ok", () => Results.Ok("fine")); + app.MapGet("/ok", () => Results.Ok("fine")); } [Fact(Timeout = 15000)] diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/ParameterBindingSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/ParameterBindingSpec.cs index 54bc37918..e1989b8d2 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/ParameterBindingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/ParameterBindingSpec.cs @@ -1,44 +1,43 @@ using System.Net; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class ParameterBindingSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/users/{id}", (int id) => + app.MapGet("/users/{id:int}", (int id) => Results.Ok(new { id })); - routeTable.Add("GET", "/search", (string q) => + app.MapGet("/search", (string q) => Results.Ok(new { query = q })); - routeTable.Add("GET", "/paged", (string q, int page) => + app.MapGet("/paged", (string q, int page) => Results.Ok(new { query = q, page })); - routeTable.Add("GET", "/with-header", + app.MapGet("/with-header", ([FromHeader(Name = "X-Tenant")] string tenant) => Results.Ok(new { tenant })); - routeTable.Add("GET", "/optional", (string? name) => + app.MapGet("/optional", (string? name) => Results.Ok(new { name = name ?? "default" })); - routeTable.Add("GET", "/items/{category}/{id}", (string category, int id) => + app.MapGet("/items/{category}/{id}", (string category, int id) => Results.Ok(new { category, id })); } diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/RequestBodySpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/RequestBodySpec.cs index 5ecca8fce..e6a42c1e0 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/RequestBodySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/RequestBodySpec.cs @@ -1,35 +1,34 @@ using System.Net; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class RequestBodySpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("POST", "/echo-body", async (TurboHttpContext ctx) => + app.MapPost("/echo-body", async (HttpContext ctx) => { using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); return Results.Ok(new { body }); }); - routeTable.Add("POST", "/echo-json", async (TurboHttpContext ctx) => + app.MapPost("/echo-json", async (HttpContext ctx) => { using var reader = new StreamReader(ctx.Request.Body); var raw = await reader.ReadToEndAsync(); @@ -37,7 +36,7 @@ protected override void ConfigureRoutes(TurboRouteTable routeTable) return Results.Ok(parsed.RootElement); }); - routeTable.Add("POST", "/form", async (TurboHttpContext ctx) => + app.MapPost("/form", async (HttpContext ctx) => { var form = await ctx.Request.ReadFormAsync(); var name = form["name"].ToString(); diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/ResponseHeadersSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/ResponseHeadersSpec.cs index 223f2b9a0..90ad094e5 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/ResponseHeadersSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/ResponseHeadersSpec.cs @@ -1,46 +1,42 @@ using System.Net; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class ResponseHeadersSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/custom-header", (TurboHttpContext ctx) => + app.MapGet("/custom-header", (HttpContext ctx) => { ctx.Response.Headers["X-Request-Id"] = "abc-123"; - ctx.Response.StatusCode = 200; - return Results.Ok("ok").ExecuteAsync(ctx); + return Results.Ok("ok"); }); - routeTable.Add("GET", "/multi-header", (TurboHttpContext ctx) => + app.MapGet("/multi-header", (HttpContext ctx) => { ctx.Response.Headers.Append("X-Tag", "alpha"); ctx.Response.Headers.Append("X-Tag", "beta"); - ctx.Response.StatusCode = 200; - return Results.Ok("ok").ExecuteAsync(ctx); + return Results.Ok("ok"); }); - routeTable.Add("GET", "/cache-headers", (TurboHttpContext ctx) => + app.MapGet("/cache-headers", (HttpContext ctx) => { - ctx.Response.Headers["Cache-Control"] = "no-cache, no-store"; - ctx.Response.Headers["ETag"] = "\"v1\""; - ctx.Response.StatusCode = 200; - return Results.Ok("cached").ExecuteAsync(ctx); + ctx.Response.Headers.CacheControl = "no-cache, no-store"; + ctx.Response.Headers.ETag = "\"v1\""; + return Results.Ok("cached"); }); } diff --git a/src/TurboHTTP.IntegrationTests.Server/Routing/RoutingEdgeCasesSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Routing/RoutingEdgeCasesSpec.cs index 17fcf6a2b..c22a28175 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Routing/RoutingEdgeCasesSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Routing/RoutingEdgeCasesSpec.cs @@ -2,35 +2,34 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Routing; public sealed class RoutingEdgeCasesSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/multi", () => + app.MapGet("/multi", () => Results.Ok(new { method = "GET" })); - routeTable.Add("POST", "/multi", () => + app.MapPost("/multi", () => Results.Ok(new { method = "POST" })); - routeTable.Add("PUT", "/multi", () => + app.MapPut("/multi", () => Results.Ok(new { method = "PUT" })); - routeTable.Add("POST", "/upload", async (TurboHttpContext ctx) => + app.MapPost("/upload", async (HttpContext ctx) => { var form = await ctx.Request.ReadFormAsync(); var file = form.Files.GetFile("document"); @@ -104,7 +103,7 @@ public async Task Multi_method_route_should_return_404_for_unregistered_method() var response = await Client.SendAsync(request, CancellationToken); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); } [Fact(Timeout = 15000)] @@ -127,10 +126,9 @@ public async Task Upload_should_receive_multipart_file() var response = await Client.SendAsync(request, CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var json = JsonDocument.Parse( - await response.Content.ReadAsStringAsync(CancellationToken)); + var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync(CancellationToken)); Assert.Equal("test.txt", json.RootElement.GetProperty("fileName").GetString()); Assert.Equal(fileBytes.Length, json.RootElement.GetProperty("size").GetInt64()); Assert.Equal(fileContent, json.RootElement.GetProperty("content").GetString()); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.IntegrationTests.Server/Shared/ServerSpecBase.cs b/src/TurboHTTP.IntegrationTests.Server/Shared/ServerSpecBase.cs index 965833fbd..844ff5784 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Shared/ServerSpecBase.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Shared/ServerSpecBase.cs @@ -2,51 +2,49 @@ using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging; -using TurboHTTP.Routing; namespace TurboHTTP.IntegrationTests.Server.Shared; public abstract class ServerSpecBase : IAsyncLifetime { - private IHost? _host; + private WebApplication? _app; private HttpClient? _client; protected ushort Port { get; private set; } protected HttpClient Client => _client!; - protected IServiceProvider Services => _host!.Services; + protected IServiceProvider Services => _app!.Services; protected static CancellationToken CancellationToken => TestContext.Current.CancellationToken; - protected abstract void ConfigureServer(IServiceCollection services, ushort port); + protected abstract void ConfigureServer(WebApplicationBuilder builder, ushort port); - protected abstract void ConfigureRoutes(TurboRouteTable routeTable); + protected abstract void ConfigureEndpoints(WebApplication app); protected virtual HttpClient? CreateHttpClient() => new(); public async ValueTask InitializeAsync() { Port = GetFreePort(); - var builder = Host.CreateApplicationBuilder(); + var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); - ConfigureServer(builder.Services, Port); - _host = builder.Build(); - ConfigureRoutes(_host.Services.GetRequiredService()); - await _host.StartAsync(); + ConfigureServer(builder, Port); + _app = builder.Build(); + ConfigureEndpoints(_app); + await _app.StartAsync(); _client = CreateHttpClient(); } public virtual async ValueTask DisposeAsync() { _client?.Dispose(); - if (_host is not null) + if (_app is not null) { - await _host.StopAsync(); - _host.Dispose(); + await _app.StopAsync(); + await _app.DisposeAsync(); } } diff --git a/src/TurboHTTP.IntegrationTests.Server/SseServerSpec.cs b/src/TurboHTTP.IntegrationTests.Server/SseServerSpec.cs index 3c439828a..f89118a10 100644 --- a/src/TurboHTTP.IntegrationTests.Server/SseServerSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/SseServerSpec.cs @@ -1,32 +1,36 @@ using System.Net; -using Akka.Streams.Dsl; +using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server; public sealed class SseServerSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/echo", () => Results.Ok("ok")); - routeTable.Add("GET", "/text", () => Results.Ok("hello world")); - routeTable.Add("GET", "/events", () => + app.MapGet("/echo", () => Results.Ok("ok")); + app.MapGet("/text", () => Results.Ok("hello world")); + app.MapGet("/events", async (HttpContext ctx) => { - var source = Source.From(["event1", "event2"]); - return TurboStreamResults.EventStream(source); + ctx.Response.ContentType = "text/event-stream"; + var events = new[] { "event1", "event2" }; + foreach (var evt in events) + { + var data = Encoding.UTF8.GetBytes($"data: {evt}\n\n"); + await ctx.Response.Body.WriteAsync(data); + } }); } diff --git a/src/TurboHTTP.IntegrationTests.Server/Streaming/RawStreamingSpec.cs b/src/TurboHTTP.IntegrationTests.Server/Streaming/RawStreamingSpec.cs index d5c5bb619..b310bc822 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Streaming/RawStreamingSpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Streaming/RawStreamingSpec.cs @@ -1,52 +1,65 @@ using System.Net; using System.Text; -using Akka.Streams.Dsl; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Streaming; public sealed class RawStreamingSpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/stream-bytes", () => + app.MapGet("/stream-bytes", () => { var chunks = new[] { - (ReadOnlyMemory)new byte[] { 1, 2, 3 }, - (ReadOnlyMemory)new byte[] { 4, 5, 6 }, - (ReadOnlyMemory)new byte[] { 7, 8, 9 } + new byte[] { 1, 2, 3 }, + new byte[] { 4, 5, 6 }, + new byte[] { 7, 8, 9 } }; - return TurboStreamResults.Stream(Source.From(chunks), "application/octet-stream"); + return Results.Stream(async stream => + { + foreach (var chunk in chunks) + { + await stream.WriteAsync(chunk); + } + }, "application/octet-stream"); }); - routeTable.Add("GET", "/stream-text", () => + app.MapGet("/stream-text", () => { var lines = new[] { "line1\n", "line2\n", "line3\n" }; - var chunks = lines.Select(l => - (ReadOnlyMemory)Encoding.UTF8.GetBytes(l)).ToArray(); - return TurboStreamResults.Stream(Source.From(chunks), "text/plain"); + return Results.Stream(async stream => + { + foreach (var line in lines) + { + await stream.WriteAsync(Encoding.UTF8.GetBytes(line)); + } + }, "text/plain"); }); - routeTable.Add("GET", "/stream-large", () => + app.MapGet("/stream-large", () => { - var chunk = new byte[1024]; - Array.Fill(chunk, (byte)0xAB); - var source = Source.From(Enumerable.Range(0, 100) - .Select(_ => (ReadOnlyMemory)chunk.ToArray().AsMemory())); - return TurboStreamResults.Stream(source, "application/octet-stream"); + return Results.Stream(async stream => + { + var chunk = new byte[1024]; + Array.Fill(chunk, (byte)0xAB); + for (var i = 0; i < 100; i++) + { + await stream.WriteAsync(chunk); + } + }, "application/octet-stream"); }); } diff --git a/src/TurboHTTP.IntegrationTests.Server/Streaming/ResponseBodySpec.cs b/src/TurboHTTP.IntegrationTests.Server/Streaming/ResponseBodySpec.cs index 67ca51eab..e86b73bf3 100644 --- a/src/TurboHTTP.IntegrationTests.Server/Streaming/ResponseBodySpec.cs +++ b/src/TurboHTTP.IntegrationTests.Server/Streaming/ResponseBodySpec.cs @@ -1,36 +1,38 @@ using System.Net; using System.Text; -using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Servus.Akka.Transport; using TurboHTTP.IntegrationTests.Server.Shared; -using TurboHTTP.Routing; using TurboHTTP.Server; namespace TurboHTTP.IntegrationTests.Server.Streaming; public sealed class ResponseBodySpec : ServerSpecBase { - protected override void ConfigureServer(IServiceCollection services, ushort port) + protected override void ConfigureServer(WebApplicationBuilder builder, ushort port) { - services.AddTurboKestrel(options => + builder.Host.UseTurboHttp(options => { options.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = port }); }); } - protected override void ConfigureRoutes(TurboRouteTable routeTable) + protected override void ConfigureEndpoints(WebApplication app) { - routeTable.Add("GET", "/stream-no-cl", () => + app.MapGet("/stream-no-cl", () => { - var chunks = new[] { "chunk1", "chunk2", "chunk3" } - .Select(s => (ReadOnlyMemory)Encoding.UTF8.GetBytes(s)) - .ToArray(); - return TurboStreamResults.Stream(Source.From(chunks), "text/plain"); + var chunks = new[] { "chunk1", "chunk2", "chunk3" }; + return Results.Stream(async stream => + { + foreach (var chunk in chunks) + { + await stream.WriteAsync(Encoding.UTF8.GetBytes(chunk)); + } + }, "text/plain"); }); - routeTable.Add("GET", "/with-cl", (TurboHttpContext ctx) => + app.MapGet("/with-cl", (HttpContext ctx) => { var body = Encoding.UTF8.GetBytes("exact-length-body"); ctx.Response.StatusCode = 200; @@ -39,9 +41,9 @@ protected override void ConfigureRoutes(TurboRouteTable routeTable) return ctx.Response.Body.WriteAsync(body).AsTask(); }); - routeTable.Add("GET", "/no-content", () => Results.NoContent()); + app.MapGet("/no-content", () => Results.NoContent()); - routeTable.Add("GET", "/not-modified", () => Results.StatusCode(304)); + app.MapGet("/not-modified", () => Results.StatusCode(304)); } [Fact(Timeout = 15000)] diff --git a/src/TurboHTTP.MicroBenchmarks/Baselines/EngineFlowBenchmark.json b/src/TurboHTTP.MicroBenchmarks/Baselines/EngineFlowBenchmark.json deleted file mode 100644 index a4a0e3a90..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Baselines/EngineFlowBenchmark.json +++ /dev/null @@ -1,1929 +0,0 @@ -{ - "Title":"TurboHTTP.MicroBenchmarks.Pipeline.EngineFlowBenchmark-20260510-072339", - "HostEnvironmentInfo":{ - "BenchmarkDotNetCaption":"BenchmarkDotNet", - "BenchmarkDotNetVersion":"0.15.8", - "OsVersion":"Windows 11 (10.0.26100.8246/24H2/2024Update/HudsonValley)", - "ProcessorName":"AMD Ryzen 5 7600X", - "PhysicalProcessorCount":1, - "PhysicalCoreCount":6, - "LogicalCoreCount":12, - "RuntimeVersion":".NET 10.0.7 (10.0.7, 10.0.726.21808)", - "Architecture":"X64", - "HasAttachedDebugger":false, - "HasRyuJit":true, - "Configuration":"RELEASE", - "DotNetCliVersion":"10.0.203", - "ChronometerFrequency":{ - "Hertz":10000000 - }, - "HardwareTimerKind":"Unknown" - }, - "Benchmarks":[ - { - "DisplayInfo":"EngineFlowBenchmark.SingleRequestRoundtrip: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Pipeline", - "Type":"EngineFlowBenchmark", - "Method":"SingleRequestRoundtrip", - "MethodTitle":"SingleRequestRoundtrip", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Pipeline.EngineFlowBenchmark.SingleRequestRoundtrip", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 10622.8271484375,10199.8046875,6781.77490234375,8244.6533203125,9525.457763671875,10167.87109375,9856.0791015625,10378.3447265625,10578.167724609375,10373.492431640625,10283.795166015625,10168.45703125,10517.236328125,7457.23876953125,7229.681396484375,7408.465576171875,7974.737548828125,8411.602783203125,10076.15966796875,9914.166259765625,11115.911865234375,10780.11474609375,10622.08251953125,10683.416748046875,10233.404541015625,10197.76611328125,10341.2841796875,10038.507080078125,10119.097900390625,9926.239013671875,11898.333740234375,11057.745361328125,6736.6455078125,7081.33544921875,6626.641845703125,8108.868408203125,8216.30859375,7828.863525390625,8639.90478515625,10579.47998046875,10252.947998046875,10186.773681640625,11298.15673828125,11002.484130859375,12518.865966796875,11661.065673828125,10974.13330078125,11287.75634765625,11283.135986328125,10799.2431640625,11922.88818359375,10667.962646484375,11898.907470703125,11222.979736328125,11186.865234375,11034.58251953125,11113.5498046875,10897.412109375,11601.69677734375,10757.40966796875,11325.634765625,10916.1376953125,12210.2783203125,14776.20849609375,11345.703125,11056.62841796875,12054.345703125,10994.86083984375,10949.71923828125,6859.295654296875,7763.7451171875,8795.330810546875,6928.338623046875,8824.52392578125,8873.9990234375,7293.9697265625,6832.53173828125,7908.282470703125,10836.566162109375,7741.485595703125,8730.682373046875,9129.461669921875,9841.217041015625,9945.599365234375,11579.4921875,9986.53564453125,10514.0625,10329.98046875,11362.066650390625,11730.23681640625,11254.168701171875,10770.782470703125,8159.967041015625,7616.650390625,8166.34521484375,7839.385986328125,6989.459228515625,7659.9609375,8600.1220703125,8756.201171875 - ], - "N":100, - "Min":6626.641845703125, - "LowerFence":4408.900451660156, - "Q1":8369.865417480469, - "Median":10243.17626953125, - "Mean":9838.207458496094, - "Q3":11010.508728027344, - "UpperFence":14971.473693847656, - "Max":14776.20849609375, - "InterquartileRange":2640.643310546875, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":164.61163957489475, - "Variance":2709699.188353506, - "StandardDeviation":1646.1163957489475, - "Skewness":-0.2518162645895054, - "Kurtosis":2.5342692026144427, - "ConfidenceInterval":{ - "N":100, - "Mean":9838.207458496094, - "StandardError":164.61163957489475, - "Level":12, - "Margin":558.2851212632577, - "Lower":9279.922337232836, - "Upper":10396.492579759351 - }, - "Percentiles":{ - "P0":6626.641845703125, - "P25":8369.865417480469, - "P50":10243.17626953125, - "P67":10786.427124023438, - "P80":11130.1025390625, - "P85":11289.31640625, - "P90":11581.712646484375, - "P95":11900.106506347656, - "P100":14776.20849609375 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":16384, - "BytesAllocatedPerOperation":7493 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":131400 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":56826100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2, - "Nanoseconds":1620400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":3, - "Nanoseconds":245800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":4, - "Nanoseconds":236700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":5, - "Nanoseconds":288800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":6, - "Nanoseconds":313000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":7, - "Nanoseconds":447600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8, - "Nanoseconds":357200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":9, - "Nanoseconds":631300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":10, - "Nanoseconds":493100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":11, - "Nanoseconds":520800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":12, - "Nanoseconds":755800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":13, - "Nanoseconds":513100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":14, - "Nanoseconds":646800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":15, - "Nanoseconds":569000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":16, - "Nanoseconds":636000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":32, - "Nanoseconds":1127700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":64, - "Nanoseconds":3126700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":128, - "Nanoseconds":5165000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":256, - "Nanoseconds":9140900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":512, - "Nanoseconds":18181100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":1024, - "Nanoseconds":36356900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":2048, - "Nanoseconds":75626400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":4096, - "Nanoseconds":166710800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":8192, - "Nanoseconds":360698400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":16384, - "Nanoseconds":517380200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16384, - "Nanoseconds":351685500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16384, - "Nanoseconds":223906400 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":16384, - "Nanoseconds":174741600 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":16384, - "Nanoseconds":160926200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":16384, - "Nanoseconds":198725300 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":16384, - "Nanoseconds":176463100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":16384, - "Nanoseconds":175837000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":16384, - "Nanoseconds":198645500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":16384, - "Nanoseconds":185659000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16384, - "Nanoseconds":174044400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16384, - "Nanoseconds":167113600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":16384, - "Nanoseconds":111112600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":16384, - "Nanoseconds":135080400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":16384, - "Nanoseconds":156065100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":16384, - "Nanoseconds":166590400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":16384, - "Nanoseconds":161482000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":16384, - "Nanoseconds":170038800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":16384, - "Nanoseconds":173312700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":16384, - "Nanoseconds":169959300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":168489700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":16384, - "Nanoseconds":166600000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":16384, - "Nanoseconds":172314400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":16384, - "Nanoseconds":122179400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":16384, - "Nanoseconds":118451100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":16384, - "Nanoseconds":121380300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":16384, - "Nanoseconds":130658100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":16384, - "Nanoseconds":137815700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":16384, - "Nanoseconds":165087800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":16384, - "Nanoseconds":162433700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":16384, - "Nanoseconds":182123100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":16384, - "Nanoseconds":176621400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":16384, - "Nanoseconds":174032200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":16384, - "Nanoseconds":175037100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":16384, - "Nanoseconds":167664100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":16384, - "Nanoseconds":167080200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":16384, - "Nanoseconds":169431600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":16384, - "Nanoseconds":164470900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":16384, - "Nanoseconds":165791300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":16384, - "Nanoseconds":162631500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":16384, - "Nanoseconds":194942300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":16384, - "Nanoseconds":181170100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":16384, - "Nanoseconds":110373200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":16384, - "Nanoseconds":116020600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":16384, - "Nanoseconds":108570900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":16384, - "Nanoseconds":132855700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":16384, - "Nanoseconds":134616000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":16384, - "Nanoseconds":128268100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":16384, - "Nanoseconds":141556200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":16384, - "Nanoseconds":173334200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":16384, - "Nanoseconds":167984300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":16384, - "Nanoseconds":166900100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":43, - "Operations":16384, - "Nanoseconds":185109000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":44, - "Operations":16384, - "Nanoseconds":180264700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":45, - "Operations":16384, - "Nanoseconds":205109100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":46, - "Operations":16384, - "Nanoseconds":191054900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":47, - "Operations":16384, - "Nanoseconds":179800200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":48, - "Operations":16384, - "Nanoseconds":184938600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":49, - "Operations":16384, - "Nanoseconds":184862900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":50, - "Operations":16384, - "Nanoseconds":176934800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":51, - "Operations":16384, - "Nanoseconds":195344600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":52, - "Operations":16384, - "Nanoseconds":174783900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":53, - "Operations":16384, - "Nanoseconds":194951700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":54, - "Operations":16384, - "Nanoseconds":183877300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":55, - "Operations":16384, - "Nanoseconds":183285600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":56, - "Operations":16384, - "Nanoseconds":180790600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":57, - "Operations":16384, - "Nanoseconds":182084400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":58, - "Operations":16384, - "Nanoseconds":178543200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":59, - "Operations":16384, - "Nanoseconds":190082200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":60, - "Operations":16384, - "Nanoseconds":176249400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":61, - "Operations":16384, - "Nanoseconds":185559200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":62, - "Operations":16384, - "Nanoseconds":178850000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":63, - "Operations":16384, - "Nanoseconds":200053200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":64, - "Operations":16384, - "Nanoseconds":242093400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":65, - "Operations":16384, - "Nanoseconds":185888000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":66, - "Operations":16384, - "Nanoseconds":181151800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":67, - "Operations":16384, - "Nanoseconds":197498400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":68, - "Operations":16384, - "Nanoseconds":180139800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":69, - "Operations":16384, - "Nanoseconds":179400200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":70, - "Operations":16384, - "Nanoseconds":112382700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":71, - "Operations":16384, - "Nanoseconds":127201200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":72, - "Operations":16384, - "Nanoseconds":144102700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":73, - "Operations":16384, - "Nanoseconds":113513900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":74, - "Operations":16384, - "Nanoseconds":144581000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":75, - "Operations":16384, - "Nanoseconds":145391600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":76, - "Operations":16384, - "Nanoseconds":119504400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":77, - "Operations":16384, - "Nanoseconds":111944200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":78, - "Operations":16384, - "Nanoseconds":129569300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":79, - "Operations":16384, - "Nanoseconds":177546300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":80, - "Operations":16384, - "Nanoseconds":126836500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":81, - "Operations":16384, - "Nanoseconds":143043500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":82, - "Operations":16384, - "Nanoseconds":149577100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":83, - "Operations":16384, - "Nanoseconds":161238500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":84, - "Operations":16384, - "Nanoseconds":162948700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":85, - "Operations":16384, - "Nanoseconds":189718400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":86, - "Operations":16384, - "Nanoseconds":163619400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":87, - "Operations":16384, - "Nanoseconds":172262400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":88, - "Operations":16384, - "Nanoseconds":169246400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":89, - "Operations":16384, - "Nanoseconds":186156100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":90, - "Operations":16384, - "Nanoseconds":192188200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":91, - "Operations":16384, - "Nanoseconds":184388300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":92, - "Operations":16384, - "Nanoseconds":176468500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":93, - "Operations":16384, - "Nanoseconds":133692900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":94, - "Operations":16384, - "Nanoseconds":124791200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":95, - "Operations":16384, - "Nanoseconds":133797400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":96, - "Operations":16384, - "Nanoseconds":128440500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":97, - "Operations":16384, - "Nanoseconds":114515300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":98, - "Operations":16384, - "Nanoseconds":125500800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":99, - "Operations":16384, - "Nanoseconds":140904400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":100, - "Operations":16384, - "Nanoseconds":143461600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16384, - "Nanoseconds":174044400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16384, - "Nanoseconds":167113600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":16384, - "Nanoseconds":111112600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":16384, - "Nanoseconds":135080400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":16384, - "Nanoseconds":156065100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":16384, - "Nanoseconds":166590400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":16384, - "Nanoseconds":161482000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":16384, - "Nanoseconds":170038800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":16384, - "Nanoseconds":173312700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":16384, - "Nanoseconds":169959300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":168489700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":16384, - "Nanoseconds":166600000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":16384, - "Nanoseconds":172314400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":16384, - "Nanoseconds":122179400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":16384, - "Nanoseconds":118451100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":16384, - "Nanoseconds":121380300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":16384, - "Nanoseconds":130658100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":16384, - "Nanoseconds":137815700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":16384, - "Nanoseconds":165087800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":16384, - "Nanoseconds":162433700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":16384, - "Nanoseconds":182123100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":16384, - "Nanoseconds":176621400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":16384, - "Nanoseconds":174032200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":16384, - "Nanoseconds":175037100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":16384, - "Nanoseconds":167664100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":16384, - "Nanoseconds":167080200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":16384, - "Nanoseconds":169431600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":16384, - "Nanoseconds":164470900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":16384, - "Nanoseconds":165791300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":16384, - "Nanoseconds":162631500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":16384, - "Nanoseconds":194942300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":16384, - "Nanoseconds":181170100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":16384, - "Nanoseconds":110373200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":16384, - "Nanoseconds":116020600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":16384, - "Nanoseconds":108570900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":16384, - "Nanoseconds":132855700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":16384, - "Nanoseconds":134616000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":16384, - "Nanoseconds":128268100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":16384, - "Nanoseconds":141556200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":16384, - "Nanoseconds":173334200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":16384, - "Nanoseconds":167984300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":16384, - "Nanoseconds":166900100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":43, - "Operations":16384, - "Nanoseconds":185109000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":44, - "Operations":16384, - "Nanoseconds":180264700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":45, - "Operations":16384, - "Nanoseconds":205109100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":46, - "Operations":16384, - "Nanoseconds":191054900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":47, - "Operations":16384, - "Nanoseconds":179800200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":48, - "Operations":16384, - "Nanoseconds":184938600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":49, - "Operations":16384, - "Nanoseconds":184862900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":50, - "Operations":16384, - "Nanoseconds":176934800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":51, - "Operations":16384, - "Nanoseconds":195344600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":52, - "Operations":16384, - "Nanoseconds":174783900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":53, - "Operations":16384, - "Nanoseconds":194951700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":54, - "Operations":16384, - "Nanoseconds":183877300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":55, - "Operations":16384, - "Nanoseconds":183285600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":56, - "Operations":16384, - "Nanoseconds":180790600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":57, - "Operations":16384, - "Nanoseconds":182084400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":58, - "Operations":16384, - "Nanoseconds":178543200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":59, - "Operations":16384, - "Nanoseconds":190082200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":60, - "Operations":16384, - "Nanoseconds":176249400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":61, - "Operations":16384, - "Nanoseconds":185559200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":62, - "Operations":16384, - "Nanoseconds":178850000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":63, - "Operations":16384, - "Nanoseconds":200053200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":64, - "Operations":16384, - "Nanoseconds":242093400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":65, - "Operations":16384, - "Nanoseconds":185888000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":66, - "Operations":16384, - "Nanoseconds":181151800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":67, - "Operations":16384, - "Nanoseconds":197498400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":68, - "Operations":16384, - "Nanoseconds":180139800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":69, - "Operations":16384, - "Nanoseconds":179400200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":70, - "Operations":16384, - "Nanoseconds":112382700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":71, - "Operations":16384, - "Nanoseconds":127201200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":72, - "Operations":16384, - "Nanoseconds":144102700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":73, - "Operations":16384, - "Nanoseconds":113513900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":74, - "Operations":16384, - "Nanoseconds":144581000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":75, - "Operations":16384, - "Nanoseconds":145391600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":76, - "Operations":16384, - "Nanoseconds":119504400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":77, - "Operations":16384, - "Nanoseconds":111944200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":78, - "Operations":16384, - "Nanoseconds":129569300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":79, - "Operations":16384, - "Nanoseconds":177546300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":80, - "Operations":16384, - "Nanoseconds":126836500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":81, - "Operations":16384, - "Nanoseconds":143043500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":82, - "Operations":16384, - "Nanoseconds":149577100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":83, - "Operations":16384, - "Nanoseconds":161238500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":84, - "Operations":16384, - "Nanoseconds":162948700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":85, - "Operations":16384, - "Nanoseconds":189718400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":86, - "Operations":16384, - "Nanoseconds":163619400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":87, - "Operations":16384, - "Nanoseconds":172262400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":88, - "Operations":16384, - "Nanoseconds":169246400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":89, - "Operations":16384, - "Nanoseconds":186156100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":90, - "Operations":16384, - "Nanoseconds":192188200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":91, - "Operations":16384, - "Nanoseconds":184388300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":92, - "Operations":16384, - "Nanoseconds":176468500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":93, - "Operations":16384, - "Nanoseconds":133692900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":94, - "Operations":16384, - "Nanoseconds":124791200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":95, - "Operations":16384, - "Nanoseconds":133797400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":96, - "Operations":16384, - "Nanoseconds":128440500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":97, - "Operations":16384, - "Nanoseconds":114515300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":98, - "Operations":16384, - "Nanoseconds":125500800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":99, - "Operations":16384, - "Nanoseconds":140904400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":100, - "Operations":16384, - "Nanoseconds":143461600 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":7493, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"EngineFlowBenchmark.SingleRequestRoundtrip: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Pipeline", - "Type":"EngineFlowBenchmark", - "Method":"SingleRequestRoundtrip", - "MethodTitle":"SingleRequestRoundtrip", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Pipeline.EngineFlowBenchmark.SingleRequestRoundtrip", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 57058200 - ], - "N":1, - "Min":57058200, - "LowerFence":57058200, - "Q1":57058200, - "Median":57058200, - "Mean":57058200, - "Q3":57058200, - "UpperFence":57058200, - "Max":57058200, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":57058200, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":57058200, - "P25":57058200, - "P50":57058200, - "P67":57058200, - "P80":57058200, - "P85":57058200, - "P90":57058200, - "P95":57058200, - "P100":57058200 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":9336 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":57058200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":57058200 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":9336, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - } - ] -} diff --git a/src/TurboHTTP.MicroBenchmarks/Baselines/Http10DecoderBenchmark.json b/src/TurboHTTP.MicroBenchmarks/Baselines/Http10DecoderBenchmark.json deleted file mode 100644 index 0024f4a35..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Baselines/Http10DecoderBenchmark.json +++ /dev/null @@ -1,2489 +0,0 @@ -{ - "Title":"TurboHTTP.MicroBenchmarks.Http10.Http10DecoderBenchmark-20260510-072145", - "HostEnvironmentInfo":{ - "BenchmarkDotNetCaption":"BenchmarkDotNet", - "BenchmarkDotNetVersion":"0.15.8", - "OsVersion":"Windows 11 (10.0.26100.8246/24H2/2024Update/HudsonValley)", - "ProcessorName":"AMD Ryzen 5 7600X", - "PhysicalProcessorCount":1, - "PhysicalCoreCount":6, - "LogicalCoreCount":12, - "RuntimeVersion":".NET 10.0.7 (10.0.7, 10.0.726.21808)", - "Architecture":"X64", - "HasAttachedDebugger":false, - "HasRyuJit":true, - "Configuration":"RELEASE", - "DotNetCliVersion":"10.0.203", - "ChronometerFrequency":{ - "Hertz":10000000 - }, - "HardwareTimerKind":"Unknown" - }, - "Benchmarks":[ - { - "DisplayInfo":"Http10DecoderBenchmark.DecodeSmallResponse: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Http10", - "Type":"Http10DecoderBenchmark", - "Method":"DecodeSmallResponse", - "MethodTitle":"DecodeSmallResponse", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Http10.Http10DecoderBenchmark.DecodeSmallResponse", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 419.6044445037842,418.09239387512207,430.3403854370117,451.34196281433105,423.93131256103516,416.0436153411865,435.23244857788086,422.67699241638184,434.4187259674072,447.3489284515381,428.1153202056885,450.4439353942871,451.1936664581299,439.7185802459717,428.82184982299805,452.11548805236816,469.12193298339844,444.25129890441895,439.8419380187988,456.7540645599365,440.4637813568115,481.6777229309082,458.7475299835205,449.03759956359863,456.8746089935303,437.3063564300537,459.1015338897705,444.9014186859131,449.24254417419434,487.6859188079834,458.64057540893555,456.6366672515869,480.93347549438477,468.3986186981201,433.3167552947998,439.2050266265869,439.2343044281006,432.79500007629395,439.3035888671875,434.93809700012207,449.2741107940674,439.37907218933105,422.43432998657227,462.9161834716797,478.57394218444824,444.78726387023926,423.2325553894043,426.3392448425293 - ], - "N":48, - "Min":416.0436153411865, - "LowerFence":397.9667663574219, - "Q1":433.18631649017334, - "Median":442.35754013061523, - "Mean":444.8913981517156, - "Q3":456.6660165786743, - "UpperFence":491.8855667114258, - "Max":487.6859188079834, - "InterquartileRange":23.479700088500977, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":2.52148729685149, - "Variance":305.17911303280476, - "StandardDeviation":17.469376435145154, - "Skewness":0.5214757223320692, - "Kurtosis":2.68709361444834, - "ConfidenceInterval":{ - "N":48, - "Mean":444.8913981517156, - "StandardError":2.52148729685149, - "Level":12, - "Margin":8.850171496648755, - "Lower":436.0412266550668, - "Upper":453.74156964836436 - }, - "Percentiles":{ - "P0":416.0436153411865, - "P25":433.18631649017334, - "P50":442.35754013061523, - "P67":450.81130361557007, - "P80":457.93418884277344, - "P85":459.083833694458, - "P90":468.6156129837036, - "P95":480.107638835907, - "P100":487.6859188079834 - } - }, - "Memory":{ - "Gen0Collections":311, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":2097152, - "BytesAllocatedPerOperation":1512 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":143300 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":7515100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":239600 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":279700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":33200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":52900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":95300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":187400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":409600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":684200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":1309300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":2565200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":22246700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":10972100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":26587100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":36315700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":62576600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":178595000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":133630800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":230405900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":452324900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":904559600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2097152, - "Nanoseconds":3027400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":2097152, - "Nanoseconds":3038900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":2097152, - "Nanoseconds":3099800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":2097152, - "Nanoseconds":3037400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":2097152, - "Nanoseconds":3128400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":2097152, - "Nanoseconds":3043400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2097152, - "Nanoseconds":3028600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":2097152, - "Nanoseconds":3055600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":2097152, - "Nanoseconds":3034500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":2097152, - "Nanoseconds":3034600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":2097152, - "Nanoseconds":3030600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":2097152, - "Nanoseconds":3061300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":2097152, - "Nanoseconds":3063500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2097152, - "Nanoseconds":3440100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":2097152, - "Nanoseconds":3232000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":2097152, - "Nanoseconds":3005800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":2097152, - "Nanoseconds":3210300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":2097152, - "Nanoseconds":3036400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":2097152, - "Nanoseconds":3041800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":2097152, - "Nanoseconds":3038100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":2097152, - "Nanoseconds":3035300 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2097152, - "Nanoseconds":882026100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":2097152, - "Nanoseconds":897579100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":2097152, - "Nanoseconds":896870400 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":2097152, - "Nanoseconds":883641800 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":2097152, - "Nanoseconds":887026300 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":2097152, - "Nanoseconds":876543500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2097152, - "Nanoseconds":883012400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":2097152, - "Nanoseconds":879841400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":2097152, - "Nanoseconds":905527300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":2097152, - "Nanoseconds":949570800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":2097152, - "Nanoseconds":892086500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":2097152, - "Nanoseconds":875544800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":2097152, - "Nanoseconds":915786700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2097152, - "Nanoseconds":889456000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":2097152, - "Nanoseconds":914080200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":2097152, - "Nanoseconds":941196800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":2097152, - "Nanoseconds":900861000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":2097152, - "Nanoseconds":947687500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":2097152, - "Nanoseconds":949259800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":2097152, - "Nanoseconds":925194800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":2097152, - "Nanoseconds":902342700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":2097152, - "Nanoseconds":951193000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":2097152, - "Nanoseconds":986858100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":934700600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":2097152, - "Nanoseconds":925453500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":2097152, - "Nanoseconds":960920800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":2097152, - "Nanoseconds":926757600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":2097152, - "Nanoseconds":1013189500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":2097152, - "Nanoseconds":965101400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":2097152, - "Nanoseconds":944738200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":2097152, - "Nanoseconds":961173600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":2097152, - "Nanoseconds":920136000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":2097152, - "Nanoseconds":965843800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":2097152, - "Nanoseconds":936064000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":2097152, - "Nanoseconds":945168000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":2097152, - "Nanoseconds":1025789600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":2097152, - "Nanoseconds":964877100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":2097152, - "Nanoseconds":960674600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":2097152, - "Nanoseconds":1011628700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":2097152, - "Nanoseconds":985341200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":2097152, - "Nanoseconds":1100082000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":2097152, - "Nanoseconds":911769200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":2097152, - "Nanoseconds":924117800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":2097152, - "Nanoseconds":924179200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":2097152, - "Nanoseconds":910675000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":2097152, - "Nanoseconds":924324500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":2097152, - "Nanoseconds":915169400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":2097152, - "Nanoseconds":945234200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":43, - "Operations":2097152, - "Nanoseconds":924482800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":44, - "Operations":2097152, - "Nanoseconds":888947100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":45, - "Operations":2097152, - "Nanoseconds":973843700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":46, - "Operations":2097152, - "Nanoseconds":1006680400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":47, - "Operations":2097152, - "Nanoseconds":935824600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":48, - "Operations":2097152, - "Nanoseconds":890621100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":49, - "Operations":2097152, - "Nanoseconds":897136300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":2097152, - "Nanoseconds":879974300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":2097152, - "Nanoseconds":876803300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":2097152, - "Nanoseconds":902489200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":2097152, - "Nanoseconds":946532700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":2097152, - "Nanoseconds":889048400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":2097152, - "Nanoseconds":872506700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":2097152, - "Nanoseconds":912748600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2097152, - "Nanoseconds":886417900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":2097152, - "Nanoseconds":911042100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":2097152, - "Nanoseconds":938158700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":2097152, - "Nanoseconds":897822900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":2097152, - "Nanoseconds":944649400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":2097152, - "Nanoseconds":946221700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":2097152, - "Nanoseconds":922156700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":2097152, - "Nanoseconds":899304600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":2097152, - "Nanoseconds":948154900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":2097152, - "Nanoseconds":983820000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":931662500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":2097152, - "Nanoseconds":922415400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":2097152, - "Nanoseconds":957882700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":2097152, - "Nanoseconds":923719500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":2097152, - "Nanoseconds":1010151400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":2097152, - "Nanoseconds":962063300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":2097152, - "Nanoseconds":941700100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":2097152, - "Nanoseconds":958135500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":2097152, - "Nanoseconds":917097900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":2097152, - "Nanoseconds":962805700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":2097152, - "Nanoseconds":933025900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":2097152, - "Nanoseconds":942129900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":2097152, - "Nanoseconds":1022751500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":2097152, - "Nanoseconds":961839000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":2097152, - "Nanoseconds":957636500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":2097152, - "Nanoseconds":1008590600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":2097152, - "Nanoseconds":982303100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":2097152, - "Nanoseconds":908731100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":2097152, - "Nanoseconds":921079700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":2097152, - "Nanoseconds":921141100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":2097152, - "Nanoseconds":907636900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":2097152, - "Nanoseconds":921286400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":2097152, - "Nanoseconds":912131300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":2097152, - "Nanoseconds":942196100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":2097152, - "Nanoseconds":921444700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":43, - "Operations":2097152, - "Nanoseconds":885909000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":44, - "Operations":2097152, - "Nanoseconds":970805600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":45, - "Operations":2097152, - "Nanoseconds":1003642300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":46, - "Operations":2097152, - "Nanoseconds":932786500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":47, - "Operations":2097152, - "Nanoseconds":887583000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":48, - "Operations":2097152, - "Nanoseconds":894098200 - } - ], - "Metrics":[ - { - "Value":0.14829635620117188, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":1512, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"Http10DecoderBenchmark.DecodeLargeResponse: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Http10", - "Type":"Http10DecoderBenchmark", - "Method":"DecodeLargeResponse", - "MethodTitle":"DecodeLargeResponse", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Http10.Http10DecoderBenchmark.DecodeLargeResponse", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 882.5708389282227,887.446403503418,935.0488662719727,970.0000762939453,973.0525970458984,971.881103515625,935.9512329101562,957.6732635498047,956.6360473632812,951.1384963989258,973.7770080566406,971.9427108764648,1001.3786315917969,972.5383758544922,945.8835601806641,922.7527618408203,1005.9995651245117,1010.3570938110352,984.3185424804688,939.4481658935547,948.6936569213867,970.1419830322266,941.7055130004883,937.468147277832,915.8332824707031,918.8716888427734,930.2663803100586,986.5100860595703,911.711311340332,993.403434753418,921.2797164916992,957.8752517700195,932.7102661132812,944.4160461425781,974.9029159545898,920.8454132080078,964.0108108520508,958.3770751953125,959.6536636352539,975.2073287963867 - ], - "N":40, - "Min":882.5708389282227, - "LowerFence":877.1601438522339, - "Q1":934.4642162322998, - "Median":957.154655456543, - "Mean":952.8419828414917, - "Q3":972.6669311523438, - "UpperFence":1029.9710035324097, - "Max":1010.3570938110352, - "InterquartileRange":38.202714920043945, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":4.710731301792314, - "Variance":887.6395759074364, - "StandardDeviation":29.793280717427486, - "Skewness":-0.23819203297507735, - "Kurtosis":2.640797366701685, - "ConfidenceInterval":{ - "N":40, - "Mean":952.8419828414917, - "StandardError":4.710731301792314, - "Level":12, - "Margin":16.761347619770167, - "Lower":936.0806352217215, - "Upper":969.6033304612619 - }, - "Percentiles":{ - "P0":882.5708389282227, - "P25":934.4642162322998, - "P50":957.154655456543, - "P67":970.3680686950684, - "P80":974.0021896362305, - "P85":976.574010848999, - "P90":987.1994209289551, - "P95":1001.6096782684326, - "P100":1010.3570938110352 - } - }, - "Memory":{ - "Gen0Collections":283, - "Gen1Collections":2, - "Gen2Collections":0, - "TotalOperations":524288, - "BytesAllocatedPerOperation":9712 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":131000 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":5613500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":346100 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":314100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":32300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":92000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":166700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":383100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":638400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":1239600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":2281000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":3163400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":7408000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":12114200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":22835900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":46404200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":122838000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":186579000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":317442100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":574283600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":524288, - "Nanoseconds":756600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":524288, - "Nanoseconds":763000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":524288, - "Nanoseconds":791200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":524288, - "Nanoseconds":761100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":524288, - "Nanoseconds":760900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":524288, - "Nanoseconds":777200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":524288, - "Nanoseconds":761100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":524288, - "Nanoseconds":779900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":524288, - "Nanoseconds":789500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":524288, - "Nanoseconds":753600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":524288, - "Nanoseconds":760400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":524288, - "Nanoseconds":769900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":524288, - "Nanoseconds":758500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":524288, - "Nanoseconds":781600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":524288, - "Nanoseconds":770400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":524288, - "Nanoseconds":763400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":524288, - "Nanoseconds":933100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":524288, - "Nanoseconds":759200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":524288, - "Nanoseconds":763200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":524288, - "Nanoseconds":762400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":524288, - "Nanoseconds":767100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":524288, - "Nanoseconds":759700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":524288, - "Nanoseconds":793550700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":524288, - "Nanoseconds":618895500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":524288, - "Nanoseconds":596423000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":524288, - "Nanoseconds":649353100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":524288, - "Nanoseconds":606946500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":524288, - "Nanoseconds":609073500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":524288, - "Nanoseconds":544300800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":524288, - "Nanoseconds":558539200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":524288, - "Nanoseconds":599735400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":524288, - "Nanoseconds":463484700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":524288, - "Nanoseconds":466040900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":524288, - "Nanoseconds":490998300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":524288, - "Nanoseconds":509322800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":524288, - "Nanoseconds":510923200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":524288, - "Nanoseconds":510309000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":524288, - "Nanoseconds":491471400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":524288, - "Nanoseconds":502860000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":524288, - "Nanoseconds":502316200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":524288, - "Nanoseconds":499433900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":524288, - "Nanoseconds":511303000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":524288, - "Nanoseconds":510341300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":524288, - "Nanoseconds":574992200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":525774200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":524288, - "Nanoseconds":510653600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":524288, - "Nanoseconds":496678800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":524288, - "Nanoseconds":484551600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":524288, - "Nanoseconds":528196900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":524288, - "Nanoseconds":530481500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":524288, - "Nanoseconds":516829800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":524288, - "Nanoseconds":493304800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":524288, - "Nanoseconds":544748300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":524288, - "Nanoseconds":498152100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":524288, - "Nanoseconds":509397200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":524288, - "Nanoseconds":494488300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":524288, - "Nanoseconds":492266700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":524288, - "Nanoseconds":480923800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":524288, - "Nanoseconds":482516800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":524288, - "Nanoseconds":488490900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":524288, - "Nanoseconds":517978800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":524288, - "Nanoseconds":478762700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":524288, - "Nanoseconds":521592900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":524288, - "Nanoseconds":483779300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":524288, - "Nanoseconds":502965900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":524288, - "Nanoseconds":489772200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":524288, - "Nanoseconds":495909400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":524288, - "Nanoseconds":547268400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":524288, - "Nanoseconds":511893300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":524288, - "Nanoseconds":483551600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":524288, - "Nanoseconds":506182700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":43, - "Operations":524288, - "Nanoseconds":503229000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":44, - "Operations":524288, - "Nanoseconds":503898300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":45, - "Operations":524288, - "Nanoseconds":512052900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":524288, - "Nanoseconds":462721300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":524288, - "Nanoseconds":465277500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":524288, - "Nanoseconds":490234900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":524288, - "Nanoseconds":508559400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":524288, - "Nanoseconds":510159800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":524288, - "Nanoseconds":509545600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":524288, - "Nanoseconds":490708000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":524288, - "Nanoseconds":502096600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":524288, - "Nanoseconds":501552800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":524288, - "Nanoseconds":498670500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":524288, - "Nanoseconds":510539600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":524288, - "Nanoseconds":509577900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":524288, - "Nanoseconds":525010800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":524288, - "Nanoseconds":509890200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":524288, - "Nanoseconds":495915400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":483788200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":524288, - "Nanoseconds":527433500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":524288, - "Nanoseconds":529718100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":524288, - "Nanoseconds":516066400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":524288, - "Nanoseconds":492541400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":524288, - "Nanoseconds":497388700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":524288, - "Nanoseconds":508633800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":524288, - "Nanoseconds":493724900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":524288, - "Nanoseconds":491503300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":524288, - "Nanoseconds":480160400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":524288, - "Nanoseconds":481753400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":524288, - "Nanoseconds":487727500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":524288, - "Nanoseconds":517215400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":524288, - "Nanoseconds":477999300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":524288, - "Nanoseconds":520829500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":524288, - "Nanoseconds":483015900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":524288, - "Nanoseconds":502202500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":524288, - "Nanoseconds":489008800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":524288, - "Nanoseconds":495146000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":524288, - "Nanoseconds":511129900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":524288, - "Nanoseconds":482788200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":524288, - "Nanoseconds":505419300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":524288, - "Nanoseconds":502465600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":524288, - "Nanoseconds":503134900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":524288, - "Nanoseconds":511289500 - } - ], - "Metrics":[ - { - "Value":0.5397796630859375, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0.003814697265625, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":9712, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"Http10DecoderBenchmark.DecodeSmallResponse: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Http10", - "Type":"Http10DecoderBenchmark", - "Method":"DecodeSmallResponse", - "MethodTitle":"DecodeSmallResponse", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Http10.Http10DecoderBenchmark.DecodeSmallResponse", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 5730100 - ], - "N":1, - "Min":5730100, - "LowerFence":5730100, - "Q1":5730100, - "Median":5730100, - "Mean":5730100, - "Q3":5730100, - "UpperFence":5730100, - "Max":5730100, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":5730100, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":5730100, - "P25":5730100, - "P50":5730100, - "P67":5730100, - "P80":5730100, - "P85":5730100, - "P90":5730100, - "P95":5730100, - "P100":5730100 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":1512 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":5730100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":5730100 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":1512, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"Http10DecoderBenchmark.DecodeLargeResponse: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Http10", - "Type":"Http10DecoderBenchmark", - "Method":"DecodeLargeResponse", - "MethodTitle":"DecodeLargeResponse", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Http10.Http10DecoderBenchmark.DecodeLargeResponse", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 6446300 - ], - "N":1, - "Min":6446300, - "LowerFence":6446300, - "Q1":6446300, - "Median":6446300, - "Mean":6446300, - "Q3":6446300, - "UpperFence":6446300, - "Max":6446300, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":6446300, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":6446300, - "P25":6446300, - "P50":6446300, - "P67":6446300, - "P80":6446300, - "P85":6446300, - "P90":6446300, - "P95":6446300, - "P100":6446300 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":9712 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":6446300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":6446300 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":9712, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - } - ] -} diff --git a/src/TurboHTTP.MicroBenchmarks/Baselines/HuffmanBenchmark.json b/src/TurboHTTP.MicroBenchmarks/Baselines/HuffmanBenchmark.json deleted file mode 100644 index 6d2ca30ae..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Baselines/HuffmanBenchmark.json +++ /dev/null @@ -1,4141 +0,0 @@ -{ - "Title":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark-20260510-072523", - "HostEnvironmentInfo":{ - "BenchmarkDotNetCaption":"BenchmarkDotNet", - "BenchmarkDotNetVersion":"0.15.8", - "OsVersion":"Windows 11 (10.0.26100.8246/24H2/2024Update/HudsonValley)", - "ProcessorName":"AMD Ryzen 5 7600X", - "PhysicalProcessorCount":1, - "PhysicalCoreCount":6, - "LogicalCoreCount":12, - "RuntimeVersion":".NET 10.0.7 (10.0.7, 10.0.726.21808)", - "Architecture":"X64", - "HasAttachedDebugger":false, - "HasRyuJit":true, - "Configuration":"RELEASE", - "DotNetCliVersion":"10.0.203", - "ChronometerFrequency":{ - "Hertz":10000000 - }, - "HardwareTimerKind":"Unknown" - }, - "Benchmarks":[ - { - "DisplayInfo":"HuffmanBenchmark.EncodeShort: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"EncodeShort", - "MethodTitle":"EncodeShort", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.EncodeShort", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 11.994856595993042,12.568573653697968,12.235504388809204,12.599627673625946,12.373507022857666,12.407958507537842,12.0728999376297,12.421298027038574,11.961932480335236,12.064030766487122,12.209759652614594,12.076134979724884,12.108303606510162,12.6678466796875,12.632220983505249 - ], - "N":15, - "Min":11.961932480335236, - "LowerFence":11.443889886140823, - "Q1":12.074517458677292, - "Median":12.235504388809204, - "Mean":12.292963663736979, - "Q3":12.49493584036827, - "UpperFence":13.12556341290474, - "Max":12.6678466796875, - "InterquartileRange":0.420418381690979, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0.06371228167829512, - "Variance":0.0608888225498163, - "StandardDeviation":0.24675660588891293, - "Skewness":0.1907282143254234, - "Kurtosis":1.3761435826556494, - "ConfidenceInterval":{ - "N":15, - "Mean":12.292963663736979, - "StandardError":0.06371228167829512, - "Level":12, - "Margin":0.2637977786014976, - "Lower":12.02916588513548, - "Upper":12.556761442338477 - }, - "Percentiles":{ - "P0":11.961932480335236, - "P25":12.074517458677292, - "P50":12.235504388809204, - "P67":12.41302752494812, - "P80":12.574784457683563, - "P85":12.596522271633148, - "P90":12.619183659553528, - "P95":12.642908692359924, - "P100":12.6678466796875 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":67108864, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":167700 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":192100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":247300 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":229900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":2700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":3900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":7000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":13700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":39100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":125000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":215300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":340500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":558300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":1094900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":2078500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":3919600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":7924100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":17529300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":31051000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":68456400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":58655200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":26597800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":4194304, - "Nanoseconds":53269400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":114707200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":16777216, - "Nanoseconds":222159900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":33554432, - "Nanoseconds":444261800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":67108864, - "Nanoseconds":952612400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":67108864, - "Nanoseconds":98415900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":67108864, - "Nanoseconds":98079400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":67108864, - "Nanoseconds":102718300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":67108864, - "Nanoseconds":70200500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":67108864, - "Nanoseconds":70051200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":67108864, - "Nanoseconds":70272500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":67108864, - "Nanoseconds":70903300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":67108864, - "Nanoseconds":70411800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":67108864, - "Nanoseconds":78280400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":67108864, - "Nanoseconds":70499500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":67108864, - "Nanoseconds":71134200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":67108864, - "Nanoseconds":70944500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":67108864, - "Nanoseconds":72170700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":67108864, - "Nanoseconds":73617600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":67108864, - "Nanoseconds":71088300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":67108864, - "Nanoseconds":71960900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":67108864, - "Nanoseconds":70520300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":67108864, - "Nanoseconds":71075500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":67108864, - "Nanoseconds":71173300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":67108864, - "Nanoseconds":71200300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":67108864, - "Nanoseconds":76081200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":67108864, - "Nanoseconds":72886000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":67108864, - "Nanoseconds":75065200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":67108864, - "Nanoseconds":900902100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":67108864, - "Nanoseconds":890792700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":67108864, - "Nanoseconds":893419600 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":67108864, - "Nanoseconds":876366200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":67108864, - "Nanoseconds":874397800 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":67108864, - "Nanoseconds":919281900 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":67108864, - "Nanoseconds":961537500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":67108864, - "Nanoseconds":934645400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":67108864, - "Nanoseconds":876161500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":67108864, - "Nanoseconds":914663000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":67108864, - "Nanoseconds":892311100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":67108864, - "Nanoseconds":916747000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":67108864, - "Nanoseconds":901572300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":67108864, - "Nanoseconds":903884300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":67108864, - "Nanoseconds":881398900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":67108864, - "Nanoseconds":904779500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":67108864, - "Nanoseconds":873952000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":67108864, - "Nanoseconds":880803700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":67108864, - "Nanoseconds":890583400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":67108864, - "Nanoseconds":881616000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":67108864, - "Nanoseconds":883774800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":67108864, - "Nanoseconds":921325100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":67108864, - "Nanoseconds":918934300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":67108864, - "Nanoseconds":804961200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":67108864, - "Nanoseconds":843462700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":67108864, - "Nanoseconds":821110800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":67108864, - "Nanoseconds":845546700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":67108864, - "Nanoseconds":830372000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":67108864, - "Nanoseconds":832684000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":67108864, - "Nanoseconds":810198600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":67108864, - "Nanoseconds":833579200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":67108864, - "Nanoseconds":802751700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":67108864, - "Nanoseconds":809603400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":67108864, - "Nanoseconds":819383100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":67108864, - "Nanoseconds":810415700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":67108864, - "Nanoseconds":812574500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":67108864, - "Nanoseconds":850124800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":67108864, - "Nanoseconds":847734000 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.EncodeLong: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"EncodeLong", - "MethodTitle":"EncodeLong", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.EncodeLong", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 83.29612016677856,86.02744340896606,82.5360655784607,80.70520162582397,80.09949922561646,84.33595895767212,82.44472742080688,79.98009920120239,82.07814693450928,80.30761480331421,87.03575134277344,80.17017841339111,89.53782320022583,86.14630699157715,88.08126449584961,84.11470651626587,87.91553974151611,85.98108291625977,83.17314386367798,85.28335094451904,82.78721570968628,90.08349180221558,89.3989086151123,86.70508861541748,90.7630443572998,87.20943927764893,85.19284725189209,88.09658288955688,84.6360445022583,83.81198644638062,86.77670955657959,90.27583599090576,87.63295412063599,83.8441014289856,87.73425817489624,85.08832454681396,84.5867395401001,85.20362377166748,84.56765413284302 - ], - "N":39, - "Min":79.98009920120239, - "LowerFence":76.954784989357, - "Q1":83.23463201522827, - "Median":85.19284725189209, - "Mean":85.22166349948981, - "Q3":87.42119669914246, - "UpperFence":93.70104372501373, - "Max":90.7630443572998, - "InterquartileRange":4.186564683914185, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0.47561175869876704, - "Variance":8.822055255488834, - "StandardDeviation":2.970194481088542, - "Skewness":-0.04074956585450287, - "Kurtosis":2.136548364273319, - "ConfidenceInterval":{ - "N":39, - "Mean":85.22166349948981, - "StandardError":0.47561175869876704, - "Level":12, - "Margin":1.6958784164350857, - "Lower":83.52578508305473, - "Upper":86.91754191592489 - }, - "Percentiles":{ - "P0":79.98009920120239, - "P25":83.23463201522827, - "P50":85.19284725189209, - "P67":86.73803424835205, - "P80":87.80677080154419, - "P85":88.08586001396179, - "P90":89.42669153213501, - "P95":90.1027262210846, - "P100":90.7630443572998 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":8388608, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":139700 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":206900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":247100 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":257900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":12800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":22300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":108500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":200800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":323000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":559600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":982300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":1771900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":3287100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":6842400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":12867700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":24937500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":51772900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":26947700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":21185000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":43834000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":90982700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":182524500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":4194304, - "Nanoseconds":372550200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":678605900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":12416700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":12166300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":12210500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":12183400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":12129600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":12342900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":12505000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":14609800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":12679700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":12172300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":12195200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":12471800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":12556500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":12262700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":12257700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":12125200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":12460000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":12132500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":12254000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":12575900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":12203400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":12088300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":12597000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":12149100 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":676241700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":692729500 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":716991200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":721516900 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":697179700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":704491300 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":699878800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":710992500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":733904500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":704616700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":689258300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":684177300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":719715300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":703850500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":683175700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":700775400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":685923100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":742362800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":684770200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":763351700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":837076900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":734901600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":8388608, - "Nanoseconds":751133200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":8388608, - "Nanoseconds":717859300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":8388608, - "Nanoseconds":749743000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":8388608, - "Nanoseconds":733515600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":709960900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":8388608, - "Nanoseconds":727662600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":8388608, - "Nanoseconds":706723500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":8388608, - "Nanoseconds":767929100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":8388608, - "Nanoseconds":864416300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":8388608, - "Nanoseconds":905872300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":8388608, - "Nanoseconds":762186400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":8388608, - "Nanoseconds":739589000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":8388608, - "Nanoseconds":773629600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":8388608, - "Nanoseconds":743819800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":8388608, - "Nanoseconds":726903400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":8388608, - "Nanoseconds":751261700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":8388608, - "Nanoseconds":722232600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":8388608, - "Nanoseconds":715319900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":8388608, - "Nanoseconds":740189800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":8388608, - "Nanoseconds":769542600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":8388608, - "Nanoseconds":747372500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":8388608, - "Nanoseconds":715589300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":8388608, - "Nanoseconds":748222300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":8388608, - "Nanoseconds":726026600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":40, - "Operations":8388608, - "Nanoseconds":721819000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":41, - "Operations":8388608, - "Nanoseconds":726993800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":42, - "Operations":8388608, - "Nanoseconds":721658900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":698738500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":721650500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":692362700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":677004300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":671923300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":707461300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":691596500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":670921700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":688521400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":673669100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":730108800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":672516200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":751097700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":722647600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":738879200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":8388608, - "Nanoseconds":705605300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":8388608, - "Nanoseconds":737489000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":8388608, - "Nanoseconds":721261600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":8388608, - "Nanoseconds":697706900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":715408600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":8388608, - "Nanoseconds":694469500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":8388608, - "Nanoseconds":755675100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":8388608, - "Nanoseconds":749932400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":8388608, - "Nanoseconds":727335000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":8388608, - "Nanoseconds":761375600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":8388608, - "Nanoseconds":731565800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":8388608, - "Nanoseconds":714649400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":8388608, - "Nanoseconds":739007700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":8388608, - "Nanoseconds":709978600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":8388608, - "Nanoseconds":703065900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":8388608, - "Nanoseconds":727935800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":8388608, - "Nanoseconds":757288600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":8388608, - "Nanoseconds":735118500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":34, - "Operations":8388608, - "Nanoseconds":703335300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":35, - "Operations":8388608, - "Nanoseconds":735968300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":36, - "Operations":8388608, - "Nanoseconds":713772600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":37, - "Operations":8388608, - "Nanoseconds":709565000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":38, - "Operations":8388608, - "Nanoseconds":714739800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":39, - "Operations":8388608, - "Nanoseconds":709404900 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.DecodeShort: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"DecodeShort", - "MethodTitle":"DecodeShort", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.DecodeShort", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 68.24531555175781,64.093017578125,65.11918306350708,64.80132341384888,62.446725368499756,64.80216979980469,65.15052318572998,63.74014616012573,67.2323226928711,62.99033164978027,67.66585111618042,62.90872097015381,64.18465375900269,63.604867458343506,61.49846315383911,67.82578229904175,63.09230327606201,68.00563335418701,61.812615394592285,62.18867301940918,62.40713596343994,65.33693075180054,62.57033348083496,63.26829195022583,65.03444910049438,62.780070304870605,66.11484289169312,66.27291440963745,67.91162490844727,64.08882141113281,66.39854907989502,62.401580810546875 - ], - "N":32, - "Min":61.49846315383911, - "LowerFence":57.95985460281372, - "Q1":62.87655830383301, - "Median":64.13883566856384, - "Mean":64.56231772899628, - "Q3":66.1543607711792, - "UpperFence":71.07106447219849, - "Max":68.24531555175781, - "InterquartileRange":3.2778024673461914, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0.3607345729145507, - "Variance":4.164141827066986, - "StandardDeviation":2.040622901730495, - "Skewness":0.3914704989913673, - "Kurtosis":1.8428180575838804, - "ConfidenceInterval":{ - "N":32, - "Mean":64.56231772899628, - "StandardError":0.3607345729145507, - "Level":12, - "Margin":1.3107133236585549, - "Lower":63.25160440533772, - "Upper":65.87303105265484 - }, - "Percentiles":{ - "P0":61.49846315383911, - "P25":62.87655830383301, - "P50":64.13883566856384, - "P67":65.14331495761871, - "P80":66.3734221458435, - "P85":67.38405764102936, - "P90":67.80978918075562, - "P95":67.95392870903015, - "P100":68.24531555175781 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":8388608, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":162200 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":434800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":229300 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":233400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":10600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":20700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":65000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":163700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":292300 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":526100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":889400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":1535200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":3069800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":5980000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":11325700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":22515600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":48765200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":46355100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":16262600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":36152000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":65812700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":2097152, - "Nanoseconds":128465900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":4194304, - "Nanoseconds":263510900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":570695500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":12146800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":12070900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":12267400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":12171100 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":12129000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":13194800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":12333400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":12204600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":12724200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":12202000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":13692300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":12436400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":12452300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":12136500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":12079600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":12067200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":12544400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":12363300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":12131900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":12348400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":13638200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":12073000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":533281800 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":519002200 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":558589700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":571916600 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":557370700 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":545099000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":552963600 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":542756200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":584831600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":549999600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":558607700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":611746400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":555941300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":536189500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":555948400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":558870600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":547039500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":576334000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":540749600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":579970700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":540065000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":550768300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":545904700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":8388608, - "Nanoseconds":528234900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":8388608, - "Nanoseconds":581312300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":8388608, - "Nanoseconds":541605000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":8388608, - "Nanoseconds":582821000 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":530870200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":8388608, - "Nanoseconds":534024800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":8388608, - "Nanoseconds":535857400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":8388608, - "Nanoseconds":560434300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":8388608, - "Nanoseconds":537226400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":8388608, - "Nanoseconds":543081300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":8388608, - "Nanoseconds":557896900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":8388608, - "Nanoseconds":538985800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":8388608, - "Nanoseconds":566959900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":8388608, - "Nanoseconds":568285900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":8388608, - "Nanoseconds":582032400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":8388608, - "Nanoseconds":549964400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":8388608, - "Nanoseconds":569339800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":33, - "Operations":8388608, - "Nanoseconds":535810800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":8388608, - "Nanoseconds":572483200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":8388608, - "Nanoseconds":537651200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":8388608, - "Nanoseconds":546259300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":8388608, - "Nanoseconds":543592900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":8388608, - "Nanoseconds":523841100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":8388608, - "Nanoseconds":543600000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":8388608, - "Nanoseconds":546522200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":8388608, - "Nanoseconds":534691100 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":8388608, - "Nanoseconds":563985600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8388608, - "Nanoseconds":528401200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":8388608, - "Nanoseconds":567622300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":8388608, - "Nanoseconds":527716600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":8388608, - "Nanoseconds":538419900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":8388608, - "Nanoseconds":533556300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":8388608, - "Nanoseconds":515886500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":8388608, - "Nanoseconds":568963900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":8388608, - "Nanoseconds":529256600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":8388608, - "Nanoseconds":570472600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":8388608, - "Nanoseconds":518521800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":8388608, - "Nanoseconds":521676400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":8388608, - "Nanoseconds":523509000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":8388608, - "Nanoseconds":548085900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":8388608, - "Nanoseconds":524878000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":8388608, - "Nanoseconds":530732900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":8388608, - "Nanoseconds":545548500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":8388608, - "Nanoseconds":526637400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":27, - "Operations":8388608, - "Nanoseconds":554611500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":28, - "Operations":8388608, - "Nanoseconds":555937500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":29, - "Operations":8388608, - "Nanoseconds":569684000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":30, - "Operations":8388608, - "Nanoseconds":537616000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":31, - "Operations":8388608, - "Nanoseconds":556991400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":32, - "Operations":8388608, - "Nanoseconds":523462400 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.DecodeLong: Job-UFXZUX(Server=True)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"DecodeLong", - "MethodTitle":"DecodeLong", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.DecodeLong", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 697.5919723510742,690.5497550964355,660.8282089233398,687.9528045654297,703.8957595825195,672.8368759155273,657.0697784423828,671.3751792907715,701.8339157104492,693.9454078674316,717.8187370300293,690.6929016113281,678.8877487182617,689.4918441772461,685.4900360107422,655.6718826293945,709.7439765930176,718.7344551086426,697.6629257202148,669.9015617370605,668.7110900878906,666.325855255127,663.0444526672363,675.6938934326172,677.613639831543 - ], - "N":25, - "Min":655.6718826293945, - "LowerFence":628.36594581604, - "Q1":669.9015617370605, - "Median":685.4900360107422, - "Mean":684.1345863342285, - "Q3":697.5919723510742, - "UpperFence":739.1275882720947, - "Max":718.7344551086426, - "InterquartileRange":27.690410614013672, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":3.651711489447102, - "Variance":333.37492005399935, - "StandardDeviation":18.25855744723551, - "Skewness":0.22767313694276642, - "Kurtosis":1.9492170919419776, - "ConfidenceInterval":{ - "N":25, - "Mean":684.1345863342285, - "StandardError":3.651711489447102, - "Level":12, - "Margin":13.677115166281036, - "Lower":670.4574711679475, - "Upper":697.8117015005096 - }, - "Percentiles":{ - "P0":655.6718826293945, - "P25":669.9015617370605, - "P50":685.4900360107422, - "P67":690.9531021118164, - "P80":698.4971237182617, - "P85":702.6586532592773, - "P90":707.4046897888184, - "P95":716.203784942627, - "P100":718.7344551086426 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1048576, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":134500 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":434600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":246500 - },{ - "IterationMode":"Workload", - "IterationStage":"Jitting", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":16, - "Nanoseconds":345800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":16, - "Nanoseconds":190400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":32, - "Nanoseconds":322600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":64, - "Nanoseconds":584800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":128, - "Nanoseconds":999500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":256, - "Nanoseconds":1648600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":512, - "Nanoseconds":3001800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1024, - "Nanoseconds":5778200 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":2048, - "Nanoseconds":10887000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":4096, - "Nanoseconds":22400000 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":8192, - "Nanoseconds":42753600 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":16384, - "Nanoseconds":52247700 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":32768, - "Nanoseconds":21821800 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":65536, - "Nanoseconds":41832500 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":131072, - "Nanoseconds":83744900 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":262144, - "Nanoseconds":169630100 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":524288, - "Nanoseconds":354218400 - },{ - "IterationMode":"Workload", - "IterationStage":"Pilot", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":683205000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1048576, - "Nanoseconds":1490700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":1048576, - "Nanoseconds":1482600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":1048576, - "Nanoseconds":1497800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":1048576, - "Nanoseconds":1529200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":1048576, - "Nanoseconds":1490000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":1048576, - "Nanoseconds":1549000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1048576, - "Nanoseconds":1490200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1048576, - "Nanoseconds":1495300 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":1048576, - "Nanoseconds":1484800 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":1048576, - "Nanoseconds":1693500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":1048576, - "Nanoseconds":1543500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":1048576, - "Nanoseconds":1500500 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":1048576, - "Nanoseconds":1538200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1048576, - "Nanoseconds":1533000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":1048576, - "Nanoseconds":1541600 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":1048576, - "Nanoseconds":1523900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":1048576, - "Nanoseconds":1519200 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":1048576, - "Nanoseconds":1533400 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":1048576, - "Nanoseconds":1568000 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":1048576, - "Nanoseconds":1482900 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":1048576, - "Nanoseconds":1508700 - },{ - "IterationMode":"Overhead", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":1048576, - "Nanoseconds":1498400 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1048576, - "Nanoseconds":675366800 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":1048576, - "Nanoseconds":711718800 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":1048576, - "Nanoseconds":736130000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":1048576, - "Nanoseconds":718042000 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":1048576, - "Nanoseconds":713133900 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":1048576, - "Nanoseconds":758386600 - },{ - "IterationMode":"Workload", - "IterationStage":"Warmup", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1048576, - "Nanoseconds":722515400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1048576, - "Nanoseconds":733002100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":1048576, - "Nanoseconds":725617800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":1048576, - "Nanoseconds":694452500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":1048576, - "Nanoseconds":722894700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":1048576, - "Nanoseconds":739612100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":1048576, - "Nanoseconds":707044500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1048576, - "Nanoseconds":690511500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":1048576, - "Nanoseconds":705511800 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":1048576, - "Nanoseconds":737450100 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":1048576, - "Nanoseconds":729178400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":1048576, - "Nanoseconds":754211400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":1048576, - "Nanoseconds":725767900 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":1048576, - "Nanoseconds":713389300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":1048576, - "Nanoseconds":784118300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":1048576, - "Nanoseconds":724508500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":1048576, - "Nanoseconds":720312300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":689045700 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":1048576, - "Nanoseconds":745744400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":1048576, - "Nanoseconds":755171600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":1048576, - "Nanoseconds":733076500 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":1048576, - "Nanoseconds":703966600 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":1048576, - "Nanoseconds":702718300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":1048576, - "Nanoseconds":700217200 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":1048576, - "Nanoseconds":696776400 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":1048576, - "Nanoseconds":710040300 - },{ - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":26, - "Operations":1048576, - "Nanoseconds":712053300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1048576, - "Nanoseconds":731478200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":2, - "Operations":1048576, - "Nanoseconds":724093900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":3, - "Operations":1048576, - "Nanoseconds":692928600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":4, - "Operations":1048576, - "Nanoseconds":721370800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":5, - "Operations":1048576, - "Nanoseconds":738088200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":6, - "Operations":1048576, - "Nanoseconds":705520600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":7, - "Operations":1048576, - "Nanoseconds":688987600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":8, - "Operations":1048576, - "Nanoseconds":703987900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":9, - "Operations":1048576, - "Nanoseconds":735926200 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":10, - "Operations":1048576, - "Nanoseconds":727654500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":11, - "Operations":1048576, - "Nanoseconds":752687500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":12, - "Operations":1048576, - "Nanoseconds":724244000 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":13, - "Operations":1048576, - "Nanoseconds":711865400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":14, - "Operations":1048576, - "Nanoseconds":722984600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":15, - "Operations":1048576, - "Nanoseconds":718788400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":16, - "Operations":1048576, - "Nanoseconds":687521800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":17, - "Operations":1048576, - "Nanoseconds":744220500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":18, - "Operations":1048576, - "Nanoseconds":753647700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":19, - "Operations":1048576, - "Nanoseconds":731552600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":20, - "Operations":1048576, - "Nanoseconds":702442700 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":21, - "Operations":1048576, - "Nanoseconds":701194400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":22, - "Operations":1048576, - "Nanoseconds":698693300 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":23, - "Operations":1048576, - "Nanoseconds":695252500 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":24, - "Operations":1048576, - "Nanoseconds":708516400 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":25, - "Operations":1048576, - "Nanoseconds":710529400 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.EncodeShort: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"EncodeShort", - "MethodTitle":"EncodeShort", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.EncodeShort", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 489800 - ], - "N":1, - "Min":489800, - "LowerFence":489800, - "Q1":489800, - "Median":489800, - "Mean":489800, - "Q3":489800, - "UpperFence":489800, - "Max":489800, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":489800, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":489800, - "P25":489800, - "P50":489800, - "P67":489800, - "P80":489800, - "P85":489800, - "P90":489800, - "P95":489800, - "P100":489800 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":489800 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":489800 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.EncodeLong: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"EncodeLong", - "MethodTitle":"EncodeLong", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.EncodeLong", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 155900 - ], - "N":1, - "Min":155900, - "LowerFence":155900, - "Q1":155900, - "Median":155900, - "Mean":155900, - "Q3":155900, - "UpperFence":155900, - "Max":155900, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":155900, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":155900, - "P25":155900, - "P50":155900, - "P67":155900, - "P80":155900, - "P85":155900, - "P90":155900, - "P95":155900, - "P100":155900 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":155900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":155900 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.DecodeShort: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"DecodeShort", - "MethodTitle":"DecodeShort", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.DecodeShort", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 451600 - ], - "N":1, - "Min":451600, - "LowerFence":451600, - "Q1":451600, - "Median":451600, - "Mean":451600, - "Q3":451600, - "UpperFence":451600, - "Max":451600, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":451600, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":451600, - "P25":451600, - "P50":451600, - "P67":451600, - "P80":451600, - "P85":451600, - "P90":451600, - "P95":451600, - "P100":451600 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":451600 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":451600 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - },{ - "DisplayInfo":"HuffmanBenchmark.DecodeLong: Dry(IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=1, WarmupCount=1)", - "Namespace":"TurboHTTP.MicroBenchmarks.Hpack", - "Type":"HuffmanBenchmark", - "Method":"DecodeLong", - "MethodTitle":"DecodeLong", - "Parameters":"", - "FullName":"TurboHTTP.MicroBenchmarks.Hpack.HuffmanBenchmark.DecodeLong", - "HardwareIntrinsics":"AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ,AVX512 IFMA+VBMI,AVX512 F+BW+CD+DQ+VL,AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE,AVX,SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT,X86Base+SSE+SSE2,AES+PCLMUL VectorSize=256", - "Statistics":{ - "OriginalValues":[ - 440900 - ], - "N":1, - "Min":440900, - "LowerFence":440900, - "Q1":440900, - "Median":440900, - "Mean":440900, - "Q3":440900, - "UpperFence":440900, - "Max":440900, - "InterquartileRange":0, - "LowerOutliers":[ - - ], - "UpperOutliers":[ - - ], - "AllOutliers":[ - - ], - "StandardError":0, - "Variance":0, - "StandardDeviation":0, - "Skewness":"", - "Kurtosis":"", - "ConfidenceInterval":{ - "N":1, - "Mean":440900, - "StandardError":0, - "Level":12, - "Margin":"", - "Lower":"", - "Upper":"" - }, - "Percentiles":{ - "P0":440900, - "P25":440900, - "P50":440900, - "P67":440900, - "P80":440900, - "P85":440900, - "P90":440900, - "P95":440900, - "P100":440900 - } - }, - "Memory":{ - "Gen0Collections":0, - "Gen1Collections":0, - "Gen2Collections":0, - "TotalOperations":1, - "BytesAllocatedPerOperation":0 - }, - "Measurements":[ - { - "IterationMode":"Workload", - "IterationStage":"Actual", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":440900 - },{ - "IterationMode":"Workload", - "IterationStage":"Result", - "LaunchIndex":1, - "IterationIndex":1, - "Operations":1, - "Nanoseconds":440900 - } - ], - "Metrics":[ - { - "Value":0, - "Descriptor":{ - "Id":"Gen0Collects", - "DisplayName":"Gen0", - "Legend":"GC Generation 0 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":0 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen1Collects", - "DisplayName":"Gen1", - "Legend":"GC Generation 1 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":1 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Gen2Collects", - "DisplayName":"Gen2", - "Legend":"GC Generation 2 collects per 1000 operations", - "NumberFormat":"#0.0000", - "UnitType":0, - "Unit":"Count", - "TheGreaterTheBetter":false, - "PriorityInCategory":2 - } - },{ - "Value":0, - "Descriptor":{ - "Id":"Allocated Memory", - "DisplayName":"Allocated", - "Legend":"Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)", - "NumberFormat":"0.##", - "UnitType":2, - "Unit":"B", - "TheGreaterTheBetter":false, - "PriorityInCategory":3 - } - } - ] - } - ] -} diff --git a/src/TurboHTTP.MicroBenchmarks/Hpack/HpackDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Hpack/HpackDecoderBenchmark.cs deleted file mode 100644 index b2606d10f..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Hpack/HpackDecoderBenchmark.cs +++ /dev/null @@ -1,64 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2.Hpack; - -namespace TurboHTTP.MicroBenchmarks.Hpack; - -[Config(typeof(MicroBenchmarkConfig))] -public class HpackDecoderBenchmark -{ - private HpackDecoder _decoder = null!; - private byte[] _typicalEncoded = null!; - private byte[] _largeEncoded = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new HpackDecoder(); - - var encoder = new HpackEncoder(useHuffman: true); - - var typicalHeaders = new List - { - new(":status", "200"), - new("content-type", "application/json"), - new("content-length", "1024"), - new("server", "nginx"), - new("date", "Sat, 10 May 2026 12:00:00 GMT"), - new("cache-control", "max-age=3600"), - new("vary", "Accept-Encoding"), - }; - - var output = new byte[4096]; - var span = output.AsSpan(); - var written = encoder.Encode(typicalHeaders, ref span); - _typicalEncoded = output[..written]; - - var largeHeaders = new List(); - largeHeaders.Add(new(":status", "200")); - for (var i = 0; i < 30; i++) - { - largeHeaders.Add(new($"x-response-header-{i}", $"value-{i}")); - } - - var largeOutput = new byte[65536]; - var largeSpan = largeOutput.AsSpan(); - var largeEncoder = new HpackEncoder(useHuffman: true); - var largeWritten = largeEncoder.Encode(largeHeaders, ref largeSpan); - _largeEncoded = largeOutput[..largeWritten]; - } - - [Benchmark(Baseline = true)] - public int DecodeTypicalHeaders() - { - var headers = _decoder.Decode(_typicalEncoded); - return headers.Count; - } - - [Benchmark] - public int Decode31Headers() - { - var headers = _decoder.Decode(_largeEncoded); - return headers.Count; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Hpack/HpackEncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Hpack/HpackEncoderBenchmark.cs deleted file mode 100644 index e3eb8e4ed..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Hpack/HpackEncoderBenchmark.cs +++ /dev/null @@ -1,56 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2.Hpack; - -namespace TurboHTTP.MicroBenchmarks.Hpack; - -[Config(typeof(MicroBenchmarkConfig))] -public class HpackEncoderBenchmark -{ - private HpackEncoder _encoder = null!; - private List _typicalHeaders = null!; - private List _largeHeaderSet = null!; - private byte[] _outputBuffer = null!; - - [GlobalSetup] - public void Setup() - { - _encoder = new HpackEncoder(useHuffman: true); - _outputBuffer = new byte[65536]; - - _typicalHeaders = - [ - new(":method", "GET"), - new(":scheme", "https"), - new(":path", "/api/v1/users"), - new(":authority", "example.com"), - new("accept", "application/json"), - new("accept-encoding", "gzip, deflate, br"), - new("user-agent", "TurboHTTP/1.0"), - ]; - - _largeHeaderSet = []; - _largeHeaderSet.Add(new(":method", "POST")); - _largeHeaderSet.Add(new(":scheme", "https")); - _largeHeaderSet.Add(new(":path", "/api/v1/data")); - _largeHeaderSet.Add(new(":authority", "example.com")); - for (var i = 0; i < 30; i++) - { - _largeHeaderSet.Add(new($"x-custom-header-{i}", $"value-{i}-with-some-content")); - } - } - - [Benchmark(Baseline = true)] - public int EncodeTypicalHeaders() - { - var span = _outputBuffer.AsSpan(); - return _encoder.Encode(_typicalHeaders, ref span); - } - - [Benchmark] - public int Encode34Headers() - { - var span = _outputBuffer.AsSpan(); - return _encoder.Encode(_largeHeaderSet, ref span); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Hpack/HuffmanBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Hpack/HuffmanBenchmark.cs deleted file mode 100644 index 0e4517ad3..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Hpack/HuffmanBenchmark.cs +++ /dev/null @@ -1,59 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol; - -namespace TurboHTTP.MicroBenchmarks.Hpack; - -[Config(typeof(MicroBenchmarkConfig))] -public class HuffmanBenchmark -{ - private byte[] _shortInput = null!; - private byte[] _longInput = null!; - private byte[] _shortEncoded = null!; - private byte[] _longEncoded = null!; - private byte[] _encodeOutput = null!; - private byte[] _decodeOutput = null!; - - [GlobalSetup] - public void Setup() - { - _shortInput = "application/json"u8.ToArray(); - _longInput = System.Text.Encoding.ASCII.GetBytes( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); - - _encodeOutput = new byte[HuffmanCodec.GetMaxEncodedLength(_longInput.Length)]; - _decodeOutput = new byte[HuffmanCodec.GetMaxDecodedLength(_longInput.Length)]; - - var shortOut = new byte[HuffmanCodec.GetMaxEncodedLength(_shortInput.Length)]; - var shortLen = HuffmanCodec.Encode(_shortInput, shortOut); - _shortEncoded = shortOut[..shortLen]; - - var longOut = new byte[HuffmanCodec.GetMaxEncodedLength(_longInput.Length)]; - var longLen = HuffmanCodec.Encode(_longInput, longOut); - _longEncoded = longOut[..longLen]; - } - - [Benchmark(Baseline = true)] - public int EncodeShort() - { - return HuffmanCodec.Encode(_shortInput, _encodeOutput); - } - - [Benchmark] - public int EncodeLong() - { - return HuffmanCodec.Encode(_longInput, _encodeOutput); - } - - [Benchmark] - public int DecodeShort() - { - return HuffmanCodec.Decode(_shortEncoded, _decodeOutput); - } - - [Benchmark] - public int DecodeLong() - { - return HuffmanCodec.Decode(_longEncoded, _decodeOutput); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http10/Http10DecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http10/Http10DecoderBenchmark.cs deleted file mode 100644 index 9bc14741d..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http10/Http10DecoderBenchmark.cs +++ /dev/null @@ -1,40 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http10.Client; -using TurboHTTP.Protocol.Syntax.Http10.Options; - -namespace TurboHTTP.MicroBenchmarks.Http10; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http10DecoderBenchmark -{ - private byte[] _smallResponse = null!; - private byte[] _largeResponse = null!; - private Http10ClientDecoder _decoder = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new Http10ClientDecoder(Http10ClientDecoderOptions.Default); - - _smallResponse = "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nHello"u8.ToArray(); - - _largeResponse = System.Text.Encoding.Latin1.GetBytes( - string.Concat("HTTP/1.0 200 OK\r\nContent-Length: 8192\r\n\r\n", - new string('X', 8192))); - } - - [Benchmark(Baseline = true)] - public object DecodeSmallResponse() - { - _decoder.Reset(); - return _decoder.Feed(_smallResponse, requestMethodWasHead: false, out _); - } - - [Benchmark] - public object DecodeLargeResponse() - { - _decoder.Reset(); - return _decoder.Feed(_largeResponse, requestMethodWasHead: false, out _); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http10/Http10EncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http10/Http10EncoderBenchmark.cs deleted file mode 100644 index 2298a7b64..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http10/Http10EncoderBenchmark.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Akka.Actor; -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http10.Client; -using TurboHTTP.Protocol.Syntax.Http10.Options; - -namespace TurboHTTP.MicroBenchmarks.Http10; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http10EncoderBenchmark -{ - private HttpRequestMessage _simpleGet = null!; - private HttpRequestMessage _requestWithHeaders = null!; - private byte[] _buffer = null!; - private Http10ClientEncoder _encoder = null!; - - [GlobalSetup] - public void Setup() - { - _buffer = new byte[16384]; - - _simpleGet = new HttpRequestMessage(HttpMethod.Get, "http://example.com/path"); - - _requestWithHeaders = new HttpRequestMessage(HttpMethod.Post, "http://example.com/api/data"); - _requestWithHeaders.Headers.TryAddWithoutValidation("Accept", "application/json"); - _requestWithHeaders.Headers.TryAddWithoutValidation("Authorization", "Bearer token123"); - _requestWithHeaders.Headers.TryAddWithoutValidation("X-Request-Id", "bench-001"); - _requestWithHeaders.Headers.TryAddWithoutValidation("Cache-Control", "no-cache"); - _requestWithHeaders.Content = new ByteArrayContent(new byte[256]); - - _encoder = new Http10ClientEncoder(Http10ClientEncoderOptions.Default); - } - - [Benchmark(Baseline = true)] - public int EncodeSimpleGet() - { - var span = _buffer.AsSpan(); - return _encoder.Encode(span, _simpleGet, ActorRefs.Nobody); - } - - [Benchmark] - public int EncodeWithHeaders() - { - var span = _buffer.AsSpan(); - return _encoder.Encode(span, _requestWithHeaders, ActorRefs.Nobody); - } -} \ No newline at end of file diff --git a/src/TurboHTTP.MicroBenchmarks/Http11/Http11ChunkedDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http11/Http11ChunkedDecoderBenchmark.cs deleted file mode 100644 index 90f4d2c86..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http11/Http11ChunkedDecoderBenchmark.cs +++ /dev/null @@ -1,54 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax; -using TurboHTTP.Protocol.Syntax.Http11.Client; -using TurboHTTP.Protocol.Syntax.Http11.Options; - -namespace TurboHTTP.MicroBenchmarks.Http11; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http11ChunkedDecoderBenchmark -{ - private byte[] _singleChunk = null!; - private byte[] _manySmallChunks = null!; - private Http11ClientDecoder _decoder = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new Http11ClientDecoder(Http11ClientDecoderOptions.Default); - - _singleChunk = System.Text.Encoding.Latin1.GetBytes( - "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + - "100\r\n" + new string('A', 256) + "\r\n0\r\n\r\n"); - - var sb = new System.Text.StringBuilder(); - sb.Append("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"); - for (var i = 0; i < 20; i++) - { - sb.Append("10\r\n"); - sb.Append(new string('B', 16)); - sb.Append("\r\n"); - } - sb.Append("0\r\n\r\n"); - _manySmallChunks = System.Text.Encoding.Latin1.GetBytes(sb.ToString()); - } - - [Benchmark(Baseline = true)] - public bool DecodeSingleChunk() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_singleChunk, false, out _); - _decoder.Reset(); - return outcome == DecodeOutcome.Complete; - } - - [Benchmark] - public bool Decode20SmallChunks() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_manySmallChunks, false, out _); - _decoder.Reset(); - return outcome == DecodeOutcome.Complete; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http11/Http11DecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http11/Http11DecoderBenchmark.cs deleted file mode 100644 index 6985caa6f..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http11/Http11DecoderBenchmark.cs +++ /dev/null @@ -1,64 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax; -using TurboHTTP.Protocol.Syntax.Http11.Client; -using TurboHTTP.Protocol.Syntax.Http11.Options; - -namespace TurboHTTP.MicroBenchmarks.Http11; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http11DecoderBenchmark -{ - private byte[] _smallResponse = null!; - private byte[] _largeResponse = null!; - private byte[] _multipleHeaders = null!; - private Http11ClientDecoder _decoder = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new Http11ClientDecoder(Http11ClientDecoderOptions.Default); - - _smallResponse = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: keep-alive\r\n\r\nHello"u8.ToArray(); - - _largeResponse = System.Text.Encoding.Latin1.GetBytes( - string.Concat("HTTP/1.1 200 OK\r\nContent-Length: 8192\r\nConnection: keep-alive\r\n\r\n", - new string('X', 8192))); - - var headers = new System.Text.StringBuilder(); - headers.Append("HTTP/1.1 200 OK\r\n"); - for (var i = 0; i < 50; i++) - { - headers.Append($"X-Header-{i}: value-{i}\r\n"); - } - headers.Append("Content-Length: 2\r\n\r\nOK"); - _multipleHeaders = System.Text.Encoding.Latin1.GetBytes(headers.ToString()); - } - - [Benchmark(Baseline = true)] - public bool DecodeSmallResponse() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_smallResponse, false, out _); - _decoder.Reset(); - return outcome == DecodeOutcome.Complete; - } - - [Benchmark] - public bool DecodeLargeResponse() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_largeResponse, false, out _); - _decoder.Reset(); - return outcome == DecodeOutcome.Complete; - } - - [Benchmark] - public bool Decode50Headers() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_multipleHeaders, false, out _); - _decoder.Reset(); - return outcome == DecodeOutcome.Complete; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http11/Http11EncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http11/Http11EncoderBenchmark.cs deleted file mode 100644 index adc4ce761..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http11/Http11EncoderBenchmark.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Akka.Actor; -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http11.Client; -using TurboHTTP.Protocol.Syntax.Http11.Options; - -namespace TurboHTTP.MicroBenchmarks.Http11; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http11EncoderBenchmark -{ - private HttpRequestMessage _simpleGet = null!; - private HttpRequestMessage _postWithBody = null!; - private byte[] _buffer = null!; - private Http11ClientEncoder _encoder = null!; - - [GlobalSetup] - public void Setup() - { - _buffer = new byte[16384]; - _encoder = new Http11ClientEncoder(Http11ClientEncoderOptions.Default); - - _simpleGet = new HttpRequestMessage(HttpMethod.Get, "http://example.com/path") - { - Version = new Version(1, 1) - }; - - _postWithBody = new HttpRequestMessage(HttpMethod.Post, "http://example.com/api/data") - { - Version = new Version(1, 1) - }; - _postWithBody.Headers.TryAddWithoutValidation("Accept", "application/json"); - _postWithBody.Headers.TryAddWithoutValidation("Authorization", "Bearer token123456789"); - _postWithBody.Headers.TryAddWithoutValidation("X-Request-Id", "perf-bench-001"); - _postWithBody.Content = new ByteArrayContent(new byte[1024]); - _postWithBody.Content.Headers.TryAddWithoutValidation("Content-Type", "application/octet-stream"); - _postWithBody.Content.Headers.ContentLength = 1024; - } - - [Benchmark(Baseline = true)] - public int EncodeSimpleGet() - { - return _encoder.Encode(_buffer.AsSpan(), _simpleGet, ActorRefs.Nobody); - } - - [Benchmark] - public int EncodePostWithBody() - { - return _encoder.Encode(_buffer.AsSpan(), _postWithBody, ActorRefs.Nobody); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameDecoderBenchmark.cs deleted file mode 100644 index 4d43e7801..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameDecoderBenchmark.cs +++ /dev/null @@ -1,82 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Servus.Akka.Transport; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2; - -namespace TurboHTTP.MicroBenchmarks.Http2; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http2FrameDecoderBenchmark -{ - private byte[] _settingsFrame = null!; - private byte[] _dataFrame = null!; - private byte[] _multipleFrames = null!; - private FrameDecoder _decoder = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new FrameDecoder(); - - _settingsFrame = - [ - 0x00, 0x00, 0x06, - 0x04, - 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, - 0x00, 0x00, 0xFF, 0xFF - ]; - - var payload = new byte[128]; - Array.Fill(payload, (byte)'D'); - _dataFrame = new byte[9 + payload.Length]; - _dataFrame[0] = 0x00; - _dataFrame[1] = 0x00; - _dataFrame[2] = (byte)payload.Length; - _dataFrame[3] = 0x00; - _dataFrame[4] = 0x01; - _dataFrame[5] = 0x00; - _dataFrame[6] = 0x00; - _dataFrame[7] = 0x00; - _dataFrame[8] = 0x01; - Array.Copy(payload, 0, _dataFrame, 9, payload.Length); - - var ms = new MemoryStream(); - for (var i = 0; i < 10; i++) - { - ms.Write(new byte[] { 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00 }); - } - _multipleFrames = ms.ToArray(); - } - - [GlobalCleanup] - public void Cleanup() => _decoder.Dispose(); - - [Benchmark(Baseline = true)] - public int DecodeSettingsFrame() - { - _decoder.Reset(); - TransportBuffer buf = _settingsFrame; - var frames = _decoder.Decode(buf); - return frames.Count; - } - - [Benchmark] - public int DecodeDataFrame() - { - _decoder.Reset(); - TransportBuffer buf = _dataFrame; - var frames = _decoder.Decode(buf); - return frames.Count; - } - - [Benchmark] - public int Decode10SettingsAck() - { - _decoder.Reset(); - TransportBuffer buf = _multipleFrames; - var frames = _decoder.Decode(buf); - return frames.Count; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameEncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameEncoderBenchmark.cs deleted file mode 100644 index 90f3bab05..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http2/Http2FrameEncoderBenchmark.cs +++ /dev/null @@ -1,49 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2.Client; - -namespace TurboHTTP.MicroBenchmarks.Http2; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http2FrameEncoderBenchmark -{ - private Http2ClientEncoder _encoder = null!; - private HttpRequestMessage _simpleGet = null!; - private HttpRequestMessage _postWithBody = null!; - private int _streamId; - - [GlobalSetup] - public void Setup() - { - _encoder = new Http2ClientEncoder(useHuffman: true); - - _simpleGet = new HttpRequestMessage(HttpMethod.Get, "https://example.com/path") - { - Version = new Version(2, 0) - }; - - _postWithBody = new HttpRequestMessage(HttpMethod.Post, "https://example.com/api/data") - { - Version = new Version(2, 0) - }; - _postWithBody.Headers.TryAddWithoutValidation("Accept", "application/json"); - _postWithBody.Headers.TryAddWithoutValidation("Authorization", "Bearer token123456789"); - _postWithBody.Content = new ByteArrayContent(new byte[1024]); - } - - [Benchmark(Baseline = true)] - public int EncodeSimpleGet() - { - _streamId += 2; - var frames = _encoder.Encode(_simpleGet, _streamId); - return frames.Count; - } - - [Benchmark] - public int EncodePostWithBody() - { - _streamId += 2; - var frames = _encoder.Encode(_postWithBody, _streamId); - return frames.Count; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Http2/Http2ResponseDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Http2/Http2ResponseDecoderBenchmark.cs deleted file mode 100644 index 7498e7611..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Http2/Http2ResponseDecoderBenchmark.cs +++ /dev/null @@ -1,44 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2; -using TurboHTTP.Protocol.Syntax.Http2.Client; -using TurboHTTP.Protocol.Syntax.Http2.Hpack; - -namespace TurboHTTP.MicroBenchmarks.Http2; - -[Config(typeof(MicroBenchmarkConfig))] -public class Http2ResponseDecoderBenchmark -{ - private HpackEncoder _hpackEncoder = null!; - private Http2ClientDecoder _responseDecoder = null!; - private byte[] _encodedHeaders = null!; - - [GlobalSetup] - public void Setup() - { - _hpackEncoder = new HpackEncoder(useHuffman: true); - _responseDecoder = new Http2ClientDecoder(); - - var headers = new List - { - new(":status", "200"), - new("content-type", "application/json"), - new("content-length", "1024"), - new("server", "TurboBench"), - new("date", "Sat, 10 May 2026 12:00:00 GMT") - }; - - var output = new byte[4096]; - var span = output.AsSpan(); - var written = _hpackEncoder.Encode(headers, ref span); - _encodedHeaders = output[..written]; - } - - [Benchmark(Baseline = true)] - public HttpResponseMessage? DecodeTypicalResponse() - { - var state = new StreamState(); - state.AppendHeader(_encodedHeaders); - return _responseDecoder.DecodeHeaders(1, endStream: true, state); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Internal/BaselineComparer.cs b/src/TurboHTTP.MicroBenchmarks/Internal/BaselineComparer.cs deleted file mode 100644 index 7ff36d744..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Internal/BaselineComparer.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System.Text; -using System.Text.Json; - -namespace TurboHTTP.MicroBenchmarks.Internal; - -public sealed record BaselineEntry( - string Method, - double MedianNanoseconds, - long AllocatedBytes); - -public sealed record ComparisonResult( - string Benchmark, - string Group, - double BaselineMedian, - double CurrentMedian, - double ThroughputDelta, - long BaselineAlloc, - long CurrentAlloc, - double AllocDelta, - bool IsNew, - bool IsRegression); - -public static class BaselineComparer -{ - private const double ThroughputRegressionThreshold = 0.10; - private const double AllocationRegressionThreshold = 0.05; - private const string NamespacePrefix = "TurboHTTP.MicroBenchmarks."; - - public static IReadOnlyList LoadBaseline(string jsonPath) - { - if (!File.Exists(jsonPath)) - { - return []; - } - - using var stream = File.OpenRead(jsonPath); - using var doc = JsonDocument.Parse(stream); - - var entries = new List(); - var benchmarks = doc.RootElement.GetProperty("Benchmarks"); - - foreach (var bm in benchmarks.EnumerateArray()) - { - var method = bm.GetProperty("FullName").GetString() ?? ""; - - if (!bm.TryGetProperty("Statistics", out var stats) - || stats.ValueKind == JsonValueKind.Null) - { - continue; - } - - var median = stats.GetProperty("Median").GetDouble(); - - long allocated = 0; - if (bm.TryGetProperty("Memory", out var mem) - && mem.ValueKind != JsonValueKind.Null - && mem.TryGetProperty("BytesAllocatedPerOperation", out var allocProp) - && allocProp.ValueKind != JsonValueKind.Null) - { - allocated = allocProp.GetInt64(); - } - - entries.Add(new BaselineEntry(method, median, allocated)); - } - - return entries; - } - - public static IReadOnlyList Compare( - IReadOnlyList baseline, - IReadOnlyList current, - string group) - { - var baselineMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var entry in baseline) - { - baselineMap.TryAdd(entry.Method, entry); - } - - var results = new List(); - - foreach (var entry in current) - { - var shortName = ShortenName(entry.Method); - - if (!baselineMap.TryGetValue(entry.Method, out var b)) - { - results.Add(new ComparisonResult( - shortName, group, - 0, entry.MedianNanoseconds, 0, - 0, entry.AllocatedBytes, 0, - IsNew: true, IsRegression: false)); - continue; - } - - var throughputDelta = b.MedianNanoseconds > 0 - ? (entry.MedianNanoseconds - b.MedianNanoseconds) / b.MedianNanoseconds - : 0; - - var allocDelta = b.AllocatedBytes > 0 - ? (double)(entry.AllocatedBytes - b.AllocatedBytes) / b.AllocatedBytes - : 0; - - var isRegression = throughputDelta > ThroughputRegressionThreshold - || allocDelta > AllocationRegressionThreshold; - - results.Add(new ComparisonResult( - shortName, group, - b.MedianNanoseconds, entry.MedianNanoseconds, throughputDelta, - b.AllocatedBytes, entry.AllocatedBytes, allocDelta, - IsNew: false, IsRegression: isRegression)); - } - - return results; - } - - public static string FormatReport(IReadOnlyList allResults) - { - if (allResults.Count == 0) - { - return "No benchmark results to compare."; - } - - var sb = new StringBuilder(); - sb.AppendLine(); - sb.AppendLine("# Benchmark Regression Report"); - sb.AppendLine(); - - var groups = allResults - .GroupBy(r => r.Group) - .OrderBy(g => g.Key); - - foreach (var group in groups) - { - sb.AppendLine(string.Concat("## ", group.Key)); - sb.AppendLine(); - sb.AppendLine("| Benchmark | Median (ns) | Delta | Alloc (B) | Delta | Status |"); - sb.AppendLine("|---|---:|---:|---:|---:|---|"); - - foreach (var r in group) - { - if (r.IsNew) - { - sb.AppendLine(string.Concat( - "| ", r.Benchmark, - " | ", $"{r.CurrentMedian:N0}", - " | NEW", - " | ", $"{r.CurrentAlloc:N0}", - " | NEW", - " | new |")); - continue; - } - - var status = r.IsRegression ? "REGRESSION" : "ok"; - sb.AppendLine(string.Concat( - "| ", r.Benchmark, - " | ", $"{r.CurrentMedian:N0}", - " | ", $"{r.ThroughputDelta:+0.0%;-0.0%;0.0%}", - " | ", $"{r.CurrentAlloc:N0}", - " | ", $"{r.AllocDelta:+0.0%;-0.0%;0.0%}", - " | ", status, " |")); - } - - sb.AppendLine(); - } - - var regressions = allResults.Where(r => r.IsRegression).ToList(); - var newBenchmarks = allResults.Where(r => r.IsNew).ToList(); - - sb.AppendLine("---"); - sb.AppendLine(); - - if (regressions.Count > 0) - { - sb.AppendLine(string.Concat("## ** ", regressions.Count.ToString(), - " Regression(s) Detected **")); - sb.AppendLine(); - foreach (var r in regressions) - { - sb.AppendLine(string.Concat( - "- **", r.Benchmark, "** [", r.Group, "]", - " — throughput ", $"{r.ThroughputDelta:+0.0%;-0.0%}", - ", alloc ", $"{r.AllocDelta:+0.0%;-0.0%}")); - } - - sb.AppendLine(); - } - else - { - sb.AppendLine("## All benchmarks within thresholds"); - sb.AppendLine(); - } - - if (newBenchmarks.Count > 0) - { - sb.AppendLine(string.Concat(newBenchmarks.Count.ToString(), - " new benchmark(s) without baseline — will be recorded as new baseline.")); - sb.AppendLine(); - } - - var total = allResults.Count; - var ok = allResults.Count(r => !r.IsRegression && !r.IsNew); - sb.AppendLine(string.Concat( - "Summary: ", total.ToString(), " benchmarks — ", - ok.ToString(), " ok, ", - regressions.Count.ToString(), " regression(s), ", - newBenchmarks.Count.ToString(), " new")); - - return sb.ToString(); - } - - public static void SaveBaseline(string sourcePath, string baselineDir, string targetName) - { - Directory.CreateDirectory(baselineDir); - File.Copy(sourcePath, Path.Combine(baselineDir, targetName), overwrite: true); - } - - private static string ShortenName(string fullName) - { - if (fullName.StartsWith(NamespacePrefix, StringComparison.Ordinal)) - { - fullName = fullName[NamespacePrefix.Length..]; - } - - var dotIndex = fullName.IndexOf('.'); - if (dotIndex >= 0) - { - fullName = fullName[(dotIndex + 1)..]; - } - - return fullName; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Internal/MicroBenchmarkConfig.cs b/src/TurboHTTP.MicroBenchmarks/Internal/MicroBenchmarkConfig.cs deleted file mode 100644 index 28cac17ec..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Internal/MicroBenchmarkConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Exporters.Json; -using BenchmarkDotNet.Jobs; - -namespace TurboHTTP.MicroBenchmarks.Internal; - -public sealed class MicroBenchmarkConfig : ManualConfig -{ - public MicroBenchmarkConfig() - { - AddJob(Job.Default.WithGcServer(true)); - AddDiagnoser(MemoryDiagnoser.Default); - AddExporter(JsonExporter.Full); - AddColumn(StatisticColumn.Median); - AddColumn(StatisticColumn.P95); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Pipeline/EngineFlowBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Pipeline/EngineFlowBenchmark.cs deleted file mode 100644 index 117e74467..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Pipeline/EngineFlowBenchmark.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Net; -using System.Threading.Channels; -using Akka.Streams; -using Akka.Streams.Dsl; -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Streams; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.MicroBenchmarks.Pipeline; - -[Config(typeof(MicroBenchmarkConfig))] -public class EngineFlowBenchmark : EngineTestBase -{ - private static readonly byte[] OkResponse = - "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"u8.ToArray(); - - private ISourceQueueWithComplete _queue = null!; - private Channel _responses = null!; - - [GlobalSetup] - public void Setup() - { - _responses = Channel.CreateUnbounded(); - - var engine = new Engine(); - var transports = new TransportRegistry() - .Register(new Version(1, 0), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(1, 1), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(2, 0), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(3, 0), CreateFakeConnectionFlow(() => OkResponse)); - var flow = engine.CreateFlow(transports, PipelineDescriptor.Empty); - - var (queue, _) = Source.Queue(16, OverflowStrategy.Backpressure) - .Via(flow) - .ToMaterialized( - Sink.ForEach(r => _responses.Writer.TryWrite(r)), - Keep.Both) - .Run(Materializer); - - _queue = queue; - } - - [GlobalCleanup] - public void Cleanup() - { - _queue.Complete(); - } - - [Benchmark(Baseline = true)] - public async Task SingleRequestRoundtrip() - { - await _queue.OfferAsync(new HttpRequestMessage(HttpMethod.Get, "http://example.com/") - { - Version = HttpVersion.Version11 - }); - - var response = await _responses.Reader.ReadAsync(); - return response.StatusCode; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Pipeline/FeedbackBufferBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Pipeline/FeedbackBufferBenchmark.cs deleted file mode 100644 index 107942e3a..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Pipeline/FeedbackBufferBenchmark.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Net; -using Akka; -using Akka.Streams.Dsl; -using BenchmarkDotNet.Attributes; -using Servus.Akka.Transport; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Semantics; -using TurboHTTP.Streams; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.MicroBenchmarks.Pipeline; - -[Config(typeof(MicroBenchmarkConfig))] -public class FeedbackBufferBenchmark : EngineTestBase -{ - private static byte[] Redirect301(string location) => - System.Text.Encoding.Latin1.GetBytes( - $"HTTP/1.1 301 Moved Permanently\r\nLocation: {location}\r\nContent-Length: 0\r\n\r\n"); - - private static byte[] Ok200() => - "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"u8.ToArray(); - - private Flow _directFlow = null!; - private Flow _redirectFlow = null!; - - private static Flow SequentialFlow(params byte[][] responses) - { - var index = 0; - return CreateFakeConnectionFlow(() => - { - var i = Interlocked.Increment(ref index) - 1; - return i < responses.Length ? responses[i] : responses[^1]; - }); - } - - private static Flow NoOpH2Flow() - => CreateFakeConnectionFlow(() => Array.Empty()); - - [GlobalSetup] - public void Setup() - { - var engine = new Engine(); - - var directTransports = new TransportRegistry() - .Register(new Version(1, 0), SequentialFlow(Ok200())) - .Register(new Version(1, 1), SequentialFlow(Ok200())) - .Register(new Version(2, 0), NoOpH2Flow()) - .Register(new Version(3, 0), NoOpH2Flow()); - _directFlow = engine.CreateFlow(directTransports, PipelineDescriptor.Empty); - - var redirectTransports = new TransportRegistry() - .Register(new Version(1, 0), SequentialFlow(Ok200())) - .Register(new Version(1, 1), SequentialFlow( - Redirect301("http://example.com/step2"), - Ok200())) - .Register(new Version(2, 0), NoOpH2Flow()) - .Register(new Version(3, 0), NoOpH2Flow()); - var redirectDescriptor = new PipelineDescriptor( - RedirectPolicy: new RedirectPolicy(), - RetryPolicy: null, - Expect100Policy: null, - CompressionPolicy: null, - CookieJar: null, - CacheStore: null, - CachePolicy: null, - Handlers: []); - _redirectFlow = engine.CreateFlow(redirectTransports, redirectDescriptor); - } - - private async Task RunSingleAsync( - Flow flow, - HttpRequestMessage request) - { - var tcs = new TaskCompletionSource(); - _ = Source.Single(request) - .Concat(Source.Never()) - .Via(flow) - .RunWith(Sink.ForEach(r => tcs.TrySetResult(r)), Materializer); - return await tcs.Task; - } - - [Benchmark(Baseline = true)] - public async Task DirectResponse() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/") - { - Version = HttpVersion.Version11 - }; - var response = await RunSingleAsync(_directFlow, request); - return response.StatusCode; - } - - [Benchmark] - public async Task SingleRedirect() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/origin") - { - Version = HttpVersion.Version11 - }; - var response = await RunSingleAsync(_redirectFlow, request); - return response.StatusCode; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Pipeline/VersionDispatchBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Pipeline/VersionDispatchBenchmark.cs deleted file mode 100644 index 70f4ece3f..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Pipeline/VersionDispatchBenchmark.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Net; -using System.Threading.Channels; -using Akka.Streams; -using Akka.Streams.Dsl; -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Streams; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.MicroBenchmarks.Pipeline; - -[Config(typeof(MicroBenchmarkConfig))] -public class VersionDispatchBenchmark : EngineTestBase -{ - private static readonly byte[] OkResponse = - "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"u8.ToArray(); - - private ISourceQueueWithComplete _queue = null!; - private Channel _responses = null!; - - [Params("1.0", "1.1")] - public string HttpVersion { get; set; } = "1.1"; - - private Version VersionValue => HttpVersion switch - { - "1.0" => new Version(1, 0), - _ => new Version(1, 1) - }; - - [GlobalSetup] - public void Setup() - { - _responses = Channel.CreateUnbounded(); - - var engine = new Engine(); - var transports = new TransportRegistry() - .Register(new Version(1, 0), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(1, 1), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(2, 0), CreateFakeConnectionFlow(() => OkResponse)) - .Register(new Version(3, 0), CreateFakeConnectionFlow(() => OkResponse)); - var flow = engine.CreateFlow(transports, PipelineDescriptor.Empty); - - var (queue, _) = Source.Queue(16, OverflowStrategy.Backpressure) - .Via(flow) - .ToMaterialized( - Sink.ForEach(r => _responses.Writer.TryWrite(r)), - Keep.Both) - .Run(Materializer); - - _queue = queue; - } - - [GlobalCleanup] - public void Cleanup() - { - _queue.Complete(); - } - - [Benchmark(Baseline = true)] - public async Task DispatchRequest() - { - await _queue.OfferAsync(new HttpRequestMessage(HttpMethod.Get, "http://example.com/") - { - Version = VersionValue - }); - - var response = await _responses.Reader.ReadAsync(); - return response.StatusCode; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Program.cs b/src/TurboHTTP.MicroBenchmarks/Program.cs deleted file mode 100644 index 714b465db..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Program.cs +++ /dev/null @@ -1,99 +0,0 @@ -using BenchmarkDotNet.Running; -using TurboHTTP.MicroBenchmarks.Internal; - -var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - -var baselineDir = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "Baselines"); -var updateBaseline = args.Contains("--update-baseline", StringComparer.OrdinalIgnoreCase); - -var allResults = new List(); -var newBaselines = new List(); - -foreach (var summary in summaries) -{ - var jsonExport = summary.ResultsDirectoryPath; - var jsonFiles = Directory.Exists(jsonExport) - ? Directory.GetFiles(jsonExport, "*-report-full.json") - : []; - - foreach (var jsonFile in jsonFiles) - { - var baselineName = ToBaselineName(Path.GetFileNameWithoutExtension(jsonFile)); - var baselinePath = Path.Combine(baselineDir, baselineName); - var baseline = BaselineComparer.LoadBaseline(baselinePath); - var current = BaselineComparer.LoadBaseline(jsonFile); - - var group = ToGroupName(baselineName); - var results = BaselineComparer.Compare(baseline, current, group); - allResults.AddRange(results); - - if (updateBaseline || baseline.Count == 0) - { - BaselineComparer.SaveBaseline(jsonFile, baselineDir, baselineName); - newBaselines.Add(baselineName); - } - } -} - -Console.WriteLine(BaselineComparer.FormatReport(allResults)); - -if (newBaselines.Count > 0) -{ - Console.WriteLine("Baselines written:"); - foreach (var name in newBaselines) - { - Console.WriteLine(string.Concat(" ", name)); - } -} - -static string ToBaselineName(string exportName) -{ - var name = exportName.Replace("-report-full", ""); - var lastDot = name.LastIndexOf('.'); - if (lastDot >= 0) - { - name = name[(lastDot + 1)..]; - } - - return name + ".json"; -} - -static string ToGroupName(string baselineName) -{ - var name = Path.GetFileNameWithoutExtension(baselineName); - - if (name.StartsWith("Hpack", StringComparison.OrdinalIgnoreCase) - || name.StartsWith("Huffman", StringComparison.OrdinalIgnoreCase)) - { - return "HPACK"; - } - - if (name.StartsWith("Http10", StringComparison.OrdinalIgnoreCase)) - { - return "HTTP/1.0"; - } - - if (name.StartsWith("Http11", StringComparison.OrdinalIgnoreCase)) - { - return "HTTP/1.1"; - } - - if (name.StartsWith("Http2", StringComparison.OrdinalIgnoreCase)) - { - return "HTTP/2"; - } - - if (name.StartsWith("Engine", StringComparison.OrdinalIgnoreCase) - || name.StartsWith("Feedback", StringComparison.OrdinalIgnoreCase) - || name.StartsWith("Version", StringComparison.OrdinalIgnoreCase)) - { - return "Pipeline"; - } - - if (name.StartsWith("Connection", StringComparison.OrdinalIgnoreCase)) - { - return "Transport"; - } - - return "Other"; -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerDecoderBenchmark.cs deleted file mode 100644 index abdbeae9f..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerDecoderBenchmark.cs +++ /dev/null @@ -1,67 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax; -using TurboHTTP.Protocol.Syntax.Http11.Options; -using TurboHTTP.Protocol.Syntax.Http11.Server; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class Http11ServerDecoderBenchmark -{ - private byte[] _simpleGet = null!; - private byte[] _postWithBody = null!; - private byte[] _manyHeaders = null!; - private Http11ServerDecoder _decoder = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new Http11ServerDecoder(Http11ServerDecoderOptions.Default); - - _simpleGet = "GET /path HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n"u8.ToArray(); - - var postBody = new byte[1024]; - Array.Fill(postBody, (byte)'X'); - _postWithBody = System.Text.Encoding.Latin1.GetBytes( - "POST /api/data HTTP/1.1\r\nHost: example.com\r\nContent-Length: 1024\r\nContent-Type: application/octet-stream\r\n\r\n"); - var combined = new byte[_postWithBody.Length + postBody.Length]; - Array.Copy(_postWithBody, 0, combined, 0, _postWithBody.Length); - Array.Copy(postBody, 0, combined, _postWithBody.Length, postBody.Length); - _postWithBody = combined; - - var headers = new System.Text.StringBuilder(); - headers.Append("GET /resource HTTP/1.1\r\n"); - headers.Append("Host: example.com\r\n"); - for (var i = 0; i < 20; i++) - { - headers.Append($"X-Custom-Header-{i}: value-{i}\r\n"); - } - headers.Append("Content-Length: 0\r\n\r\n"); - _manyHeaders = System.Text.Encoding.Latin1.GetBytes(headers.ToString()); - } - - [Benchmark(Baseline = true)] - public bool DecodeSimpleGet() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_simpleGet, out _); - return outcome == DecodeOutcome.Complete; - } - - [Benchmark] - public bool DecodePostWithBody() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_postWithBody, out _); - return outcome == DecodeOutcome.Complete; - } - - [Benchmark] - public bool DecodeManyHeaders() - { - _decoder.Reset(); - var outcome = _decoder.Feed(_manyHeaders, out _); - return outcome == DecodeOutcome.Complete; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerEncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerEncoderBenchmark.cs deleted file mode 100644 index 19584f2e6..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/Http11ServerEncoderBenchmark.cs +++ /dev/null @@ -1,70 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http11.Options; -using TurboHTTP.Protocol.Syntax.Http11.Server; -using TurboHTTP.Server; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class Http11ServerEncoderBenchmark -{ - private byte[] _buffer = null!; - private Http11ServerEncoder _encoder = null!; - private TurboHttpContext _simpleOkContext = null!; - private TurboHttpContext _withBodyContext = null!; - private TurboHttpContext _manyHeadersContext = null!; - - [GlobalSetup] - public void Setup() - { - _buffer = new byte[16384]; - _encoder = new Http11ServerEncoder(Http11ServerEncoderOptions.Default); - - _simpleOkContext = CreateContext(200, contentLength: 0); - - _withBodyContext = CreateContext(200, contentLength: 1024); - _withBodyContext.Response.Headers["Content-Type"] = "application/octet-stream"; - - _manyHeadersContext = CreateContext(200, contentLength: 0); - for (var i = 0; i < 10; i++) - { - _manyHeadersContext.Response.Headers[$"X-Custom-Header-{i}"] = $"value-{i}"; - } - } - - [Benchmark(Baseline = true)] - public int EncodeSimpleOk() - { - return _encoder.Encode(_buffer.AsSpan(), _simpleOkContext, isChunked: false, connectionClose: false); - } - - [Benchmark] - public int EncodeWithBody() - { - return _encoder.Encode(_buffer.AsSpan(), _withBodyContext, isChunked: false, connectionClose: false); - } - - [Benchmark] - public int EncodeWithManyHeaders() - { - return _encoder.Encode(_buffer.AsSpan(), _manyHeadersContext, isChunked: false, connectionClose: false); - } - - private static TurboHttpContext CreateContext(int statusCode, long contentLength) - { - var features = new FeatureCollection(); - features.Set(new TurboHttpRequestFeature()); - var responseFeature = new TurboHttpResponseFeature { StatusCode = statusCode }; - features.Set(responseFeature); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - - var context = new TurboHttpContext(features); - context.Response.ContentLength = contentLength; - return context; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerDecoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerDecoderBenchmark.cs deleted file mode 100644 index fe2581b50..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerDecoderBenchmark.cs +++ /dev/null @@ -1,104 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.Context.Features; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol; -using TurboHTTP.Protocol.Syntax.Http2; -using TurboHTTP.Protocol.Syntax.Http2.Hpack; -using TurboHTTP.Protocol.Syntax.Http2.Server; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class Http2ServerDecoderBenchmark -{ - private Http2ServerDecoder _decoder = null!; - private byte[] _simpleGetHeaders = null!; - private byte[] _postWithHeadersAndBody = null!; - private byte[] _manyHeaders = null!; - - [GlobalSetup] - public void Setup() - { - _decoder = new Http2ServerDecoder(maxHeaderSize: 16 * 1024, maxTotalHeaderSize: 64 * 1024); - - var hpackEncoder = new HpackEncoder(useHuffman: true); - - // Simple GET request: :method, :path, :scheme, :authority - var simpleHeaders = new List - { - new(WellKnownHeaders.Method, "GET"), - new(WellKnownHeaders.Path, "/path"), - new(WellKnownHeaders.Scheme, "https"), - new(WellKnownHeaders.Authority, "example.com") - }; - var output = new byte[4096]; - var span = output.AsSpan(); - var written = hpackEncoder.Encode(simpleHeaders, ref span); - _simpleGetHeaders = output[..written]; - - // Reset encoder for next benchmark - hpackEncoder = new HpackEncoder(useHuffman: true); - - // POST with body - var postHeaders = new List - { - new(WellKnownHeaders.Method, "POST"), - new(WellKnownHeaders.Path, "/api/data"), - new(WellKnownHeaders.Scheme, "https"), - new(WellKnownHeaders.Authority, "example.com"), - new("content-type", "application/json"), - new("content-length", "1024") - }; - output = new byte[4096]; - span = output.AsSpan(); - written = hpackEncoder.Encode(postHeaders, ref span); - _postWithHeadersAndBody = output[..written]; - - // Reset encoder for next benchmark - hpackEncoder = new HpackEncoder(useHuffman: true); - - // Many headers - var manyHeadersList = new List - { - new(WellKnownHeaders.Method, "GET"), - new(WellKnownHeaders.Path, "/resource"), - new(WellKnownHeaders.Scheme, "https"), - new(WellKnownHeaders.Authority, "example.com") - }; - for (var i = 0; i < 20; i++) - { - manyHeadersList.Add(new($"x-custom-header-{i}", $"value-{i}")); - } - output = new byte[4096]; - span = output.AsSpan(); - written = hpackEncoder.Encode(manyHeadersList, ref span); - _manyHeaders = output[..written]; - } - - [Benchmark(Baseline = true)] - public object? DecodeSimpleGet() - { - _decoder.ResetHpack(); - var state = new StreamState(); - state.AppendHeader(_simpleGetHeaders); - return _decoder.DecodeHeadersToFeature(streamId: 1, endStream: true, state); - } - - [Benchmark] - public object? DecodePostWithHeaders() - { - _decoder.ResetHpack(); - var state = new StreamState(); - state.AppendHeader(_postWithHeadersAndBody); - return _decoder.DecodeHeadersToFeature(streamId: 3, endStream: false, state); - } - - [Benchmark] - public object? DecodeManyHeaders() - { - _decoder.ResetHpack(); - var state = new StreamState(); - state.AppendHeader(_manyHeaders); - return _decoder.DecodeHeadersToFeature(streamId: 5, endStream: true, state); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerEncoderBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerEncoderBenchmark.cs deleted file mode 100644 index 497cb27e4..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/Http2ServerEncoderBenchmark.cs +++ /dev/null @@ -1,73 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Protocol.Syntax.Http2.Server; -using TurboHTTP.Server; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class Http2ServerEncoderBenchmark -{ - private Http2ServerEncoder _encoder = null!; - private TurboHttpContext _simpleOkContext = null!; - private TurboHttpContext _withBodyContext = null!; - private TurboHttpContext _manyHeadersContext = null!; - private int _streamId; - - [GlobalSetup] - public void Setup() - { - _encoder = new Http2ServerEncoder(); - _streamId = 1; - - _simpleOkContext = CreateContext(200); - - _withBodyContext = CreateContext(200); - _withBodyContext.Response.Headers["Content-Type"] = "application/json"; - _withBodyContext.Response.ContentLength = 1024; - - _manyHeadersContext = CreateContext(200); - for (var i = 0; i < 10; i++) - { - _manyHeadersContext.Response.Headers[$"X-Custom-Header-{i}"] = $"value-{i}"; - } - } - - [Benchmark(Baseline = true)] - public int EncodeSimpleOk() - { - _streamId += 2; - var frames = _encoder.EncodeHeaders(_simpleOkContext, _streamId, hasBody: false); - return frames.Count; - } - - [Benchmark] - public int EncodeResponseWithBody() - { - _streamId += 2; - var frames = _encoder.EncodeHeaders(_withBodyContext, _streamId, hasBody: true); - return frames.Count; - } - - [Benchmark] - public int EncodeWithManyHeaders() - { - _streamId += 2; - var frames = _encoder.EncodeHeaders(_manyHeadersContext, _streamId, hasBody: false); - return frames.Count; - } - - private static TurboHttpContext CreateContext(int statusCode) - { - var features = new FeatureCollection(); - features.Set(new TurboHttpRequestFeature()); - var responseFeature = new TurboHttpResponseFeature { StatusCode = statusCode }; - features.Set(responseFeature); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/RouteTableMatchBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/RouteTableMatchBenchmark.cs deleted file mode 100644 index b17ce67cf..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/RouteTableMatchBenchmark.cs +++ /dev/null @@ -1,138 +0,0 @@ -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Routing; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class RouteTableMatchBenchmark -{ - private RouteTable _staticTable10 = null!; - private RouteTable _staticTable100 = null!; - private RouteTable _parameterizedTable10 = null!; - private RouteTable _parameterizedTable100 = null!; - private RouteTable _mixedTable = null!; - private RouteTable _noMatchTable = null!; - - private string _staticPath10 = null!; - private string _staticPath100 = null!; - private string _parameterizedPath10 = null!; - private string _parameterizedPath100 = null!; - private string _mixedStaticPath = null!; - private string _mixedParameterizedPath = null!; - private string _noMatchPath = null!; - - [GlobalSetup] - public void Setup() - { - var noOpDispatcher = new DelegateDispatcher(_ => Task.CompletedTask); - - // Static routes with 10 entries - var staticBuilder10 = new RouteTableBuilder(); - for (int i = 0; i < 10; i++) - { - staticBuilder10.Add("GET", $"/api/endpoint{i}", noOpDispatcher); - } - _staticTable10 = staticBuilder10.Build(); - _staticPath10 = "/api/endpoint9"; - - // Static routes with 100 entries - var staticBuilder100 = new RouteTableBuilder(); - for (int i = 0; i < 100; i++) - { - staticBuilder100.Add("GET", $"/api/endpoint{i}", noOpDispatcher); - } - _staticTable100 = staticBuilder100.Build(); - _staticPath100 = "/api/endpoint99"; - - // Parameterized routes with 10 entries - var paramBuilder10 = new RouteTableBuilder(); - for (int i = 0; i < 10; i++) - { - paramBuilder10.Add("GET", $"/items/{i}/details", noOpDispatcher); - } - _parameterizedTable10 = paramBuilder10.Build(); - _parameterizedPath10 = "/items/5/details"; - - // Parameterized routes with 100 entries - var paramBuilder100 = new RouteTableBuilder(); - for (int i = 0; i < 100; i++) - { - paramBuilder100.Add("GET", $"/items/{i}/details", noOpDispatcher); - } - _parameterizedTable100 = paramBuilder100.Build(); - _parameterizedPath100 = "/items/50/details"; - - // Mixed routes: 50 static + 50 parameterized - var mixedBuilder = new RouteTableBuilder(); - for (int i = 0; i < 50; i++) - { - mixedBuilder.Add("GET", $"/static/path{i}", noOpDispatcher); - } - for (int i = 0; i < 50; i++) - { - mixedBuilder.Add("POST", $"/dynamic/{i}/data", noOpDispatcher); - } - _mixedTable = mixedBuilder.Build(); - _mixedStaticPath = "/static/path25"; - _mixedParameterizedPath = "/dynamic/25/data"; - - // No-match table: 100 routes that won't match - var noMatchBuilder = new RouteTableBuilder(); - for (int i = 0; i < 100; i++) - { - noMatchBuilder.Add("GET", $"/nomatch/endpoint{i}", noOpDispatcher); - } - _noMatchTable = noMatchBuilder.Build(); - _noMatchPath = "/completely/different/path"; - } - - [Benchmark(Baseline = true)] - public bool StaticRoute_10Entries() - { - var result = _staticTable10.Match("GET", _staticPath10); - return result.IsMatch; - } - - [Benchmark] - public bool StaticRoute_100Entries() - { - var result = _staticTable100.Match("GET", _staticPath100); - return result.IsMatch; - } - - [Benchmark] - public bool ParameterizedRoute_10Entries() - { - var result = _parameterizedTable10.Match("GET", _parameterizedPath10); - return result.IsMatch; - } - - [Benchmark] - public bool ParameterizedRoute_100Entries() - { - var result = _parameterizedTable100.Match("GET", _parameterizedPath100); - return result.IsMatch; - } - - [Benchmark] - public bool MixedRoutes_StaticHit() - { - var result = _mixedTable.Match("GET", _mixedStaticPath); - return result.IsMatch; - } - - [Benchmark] - public bool MixedRoutes_ParameterizedHit() - { - var result = _mixedTable.Match("POST", _mixedParameterizedPath); - return result.IsMatch; - } - - [Benchmark] - public bool NoMatch() - { - var result = _noMatchTable.Match("GET", _noMatchPath); - return result.IsMatch; - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Server/ServerContextFactoryBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Server/ServerContextFactoryBenchmark.cs deleted file mode 100644 index 371b74768..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Server/ServerContextFactoryBenchmark.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Net; -using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; -using TurboHTTP.MicroBenchmarks.Internal; -using TurboHTTP.Server; - -namespace TurboHTTP.MicroBenchmarks.Server; - -[Config(typeof(MicroBenchmarkConfig))] -public sealed class ServerContextFactoryBenchmark -{ - private TurboHttpRequestFeature _requestFeature = null!; - private TurboConnectionInfo _connectionInfo = null!; - private IServiceProvider _serviceProvider = null!; - - [GlobalSetup] - public void Setup() - { - _requestFeature = new TurboHttpRequestFeature - { - Method = "GET", - Path = "/api/test", - Protocol = "HTTP/1.1", - Scheme = "http", - Headers = new HeaderDictionary(), - Body = Stream.Null - }; - - _connectionInfo = new TurboConnectionInfo( - id: Guid.NewGuid().ToString("N"), - remoteIpAddress: IPAddress.Parse("127.0.0.1"), - remotePort: 12345, - localIpAddress: IPAddress.Parse("127.0.0.1"), - localPort: 80); - - // Create a minimal service provider - _serviceProvider = new MinimalServiceProvider(); - } - - [Benchmark(Baseline = true)] - public TurboHttpContext Create_WithPooling() - { - return ServerContextFactory.Create( - _requestFeature, - hasBody: false, - services: _serviceProvider, - connectionInfo: _connectionInfo); - } - - [Benchmark] - public void Create_Return_Cycle() - { - var context = ServerContextFactory.Create( - _requestFeature, - hasBody: false, - services: _serviceProvider, - connectionInfo: _connectionInfo); - - ServerContextFactory.Return(context); - } - - [Benchmark] - public TurboHttpContext Create_WithoutPooling() - { - return ServerContextFactory.Create( - _requestFeature, - hasBody: false, - services: null, - connectionInfo: null); - } - - /// - /// Minimal service provider implementation for benchmarking. - /// - private sealed class MinimalServiceProvider : IServiceProvider - { - public object? GetService(Type serviceType) - { - return null; - } - } -} diff --git a/src/TurboHTTP.MicroBenchmarks/Transport/ConnectionSetupBenchmark.cs b/src/TurboHTTP.MicroBenchmarks/Transport/ConnectionSetupBenchmark.cs deleted file mode 100644 index ca381545e..000000000 --- a/src/TurboHTTP.MicroBenchmarks/Transport/ConnectionSetupBenchmark.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using BenchmarkDotNet.Attributes; -using TurboHTTP.MicroBenchmarks.Internal; - -namespace TurboHTTP.MicroBenchmarks.Transport; - -[Config(typeof(MicroBenchmarkConfig))] -public class ConnectionSetupBenchmark -{ - private TcpListener _listener = null!; - private int _port; - private Task _acceptLoop = null!; - private CancellationTokenSource _cts = null!; - - [GlobalSetup] - public void Setup() - { - _cts = new CancellationTokenSource(); - _listener = new TcpListener(IPAddress.Loopback, 0); - _listener.Start(); - _port = ((IPEndPoint)_listener.LocalEndpoint).Port; - - _acceptLoop = Task.Run(async () => - { - while (!_cts.Token.IsCancellationRequested) - { - try - { - var client = await _listener.AcceptTcpClientAsync(_cts.Token); - client.Close(); - } - catch (OperationCanceledException) - { - break; - } - } - }); - } - - [GlobalCleanup] - public void Cleanup() - { - _cts.Cancel(); - _listener.Stop(); - _acceptLoop.Wait(TimeSpan.FromSeconds(5)); - _cts.Dispose(); - } - - [Benchmark(Baseline = true)] - public async Task TcpLoopbackConnect() - { - using var client = new TcpClient(); - await client.ConnectAsync(IPAddress.Loopback, _port); - } -} diff --git a/src/TurboHTTP.StressBenchmarks/IStressScenario.cs b/src/TurboHTTP.StressBenchmarks/IStressScenario.cs new file mode 100644 index 000000000..15bb7ec05 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/IStressScenario.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace TurboHTTP.StressBenchmarks; + +public interface IStressScenario +{ + string Name { get; } + StressRunConfig DefaultConfig { get; } + void ConfigureRoutes(WebApplication app); + Func> CreateRequestFunc(); +} diff --git a/src/TurboHTTP.StressBenchmarks/LoadGenerator.cs b/src/TurboHTTP.StressBenchmarks/LoadGenerator.cs new file mode 100644 index 000000000..9871e2508 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/LoadGenerator.cs @@ -0,0 +1,65 @@ +using System.Diagnostics; + +namespace TurboHTTP.StressBenchmarks; + +public static class LoadGenerator +{ + public static async Task RunAsync( + Uri baseUri, + StressRunConfig config, + Func> requestFunc, + Action onResult, + CancellationToken ct) + { + using var handler = new SocketsHttpHandler + { + MaxConnectionsPerServer = config.Concurrency, + PooledConnectionLifetime = config.DisableKeepAlive + ? TimeSpan.Zero + : TimeSpan.FromMinutes(5), + EnableMultipleHttp2Connections = true, + }; + + using var client = new HttpClient(handler) + { + BaseAddress = baseUri, + Timeout = TimeSpan.FromSeconds(30), + }; + + var tasks = new Task[config.Concurrency]; + for (var i = 0; i < config.Concurrency; i++) + { + tasks[i] = WorkerLoop(client, baseUri, requestFunc, onResult, ct); + } + + await Task.WhenAll(tasks); + } + + private static async Task WorkerLoop( + HttpClient client, + Uri baseUri, + Func> requestFunc, + Action onResult, + CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + var start = Stopwatch.GetTimestamp(); + try + { + using var response = await requestFunc(client, baseUri); + var elapsed = Stopwatch.GetElapsedTime(start).TotalMilliseconds; + onResult(new RequestResult((int)response.StatusCode, elapsed, null)); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + var elapsed = Stopwatch.GetElapsedTime(start).TotalMilliseconds; + onResult(new RequestResult(0, elapsed, ex)); + } + } + } +} diff --git a/src/TurboHTTP.StressBenchmarks/MetricsCollector.cs b/src/TurboHTTP.StressBenchmarks/MetricsCollector.cs new file mode 100644 index 000000000..0077de265 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/MetricsCollector.cs @@ -0,0 +1,140 @@ +using System.Diagnostics; + +namespace TurboHTTP.StressBenchmarks; + +public sealed class MetricsCollector +{ + private readonly object _lock = new(); + private readonly long _startTimestamp; + private readonly Dictionary> _latencyBuckets = []; + private readonly Dictionary _errorBuckets = []; + private readonly List<(int Second, long MemoryBytes, int GcGen0, int GcGen1, int GcGen2)> _memorySnapshots = []; + private readonly Timer _memoryTimer; + private int _gcGen0Baseline; + private int _gcGen1Baseline; + private int _gcGen2Baseline; + + public MetricsCollector() + { + _gcGen0Baseline = GC.CollectionCount(0); + _gcGen1Baseline = GC.CollectionCount(1); + _gcGen2Baseline = GC.CollectionCount(2); + _startTimestamp = Stopwatch.GetTimestamp(); + _memoryTimer = new Timer(SampleMemory, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } + + public void Record(RequestResult result) + { + var elapsed = Stopwatch.GetElapsedTime(_startTimestamp); + var second = (int)elapsed.TotalSeconds; + + lock (_lock) + { + if (!_latencyBuckets.TryGetValue(second, out var bucket)) + { + bucket = new List(512); + _latencyBuckets[second] = bucket; + } + bucket.Add(result.ElapsedMs); + + if (result.Error is not null || result.StatusCode >= 400) + { + _errorBuckets.TryGetValue(second, out var count); + _errorBuckets[second] = count + 1; + } + } + } + + public StressResult Build(ServerType server, StressRunConfig config) + { + _memoryTimer.Dispose(); + + List slices; + lock (_lock) + { + var maxSecond = _latencyBuckets.Count > 0 ? _latencyBuckets.Keys.Max() : 0; + slices = new List(maxSecond + 1); + + for (var s = 0; s <= maxSecond; s++) + { + var latencies = _latencyBuckets.TryGetValue(s, out var bucket) ? bucket : []; + _errorBuckets.TryGetValue(s, out var errors); + + var snapshot = _memorySnapshots.FirstOrDefault(m => m.Second == s); + + double p50 = 0, p95 = 0, p99 = 0; + if (latencies.Count > 0) + { + latencies.Sort(); + p50 = Percentile(latencies, 0.50); + p95 = Percentile(latencies, 0.95); + p99 = Percentile(latencies, 0.99); + } + + slices.Add(new TimeSlice( + s, + latencies.Count, + errors, + p50, + p95, + p99, + snapshot.MemoryBytes, + snapshot.GcGen0, + snapshot.GcGen1, + snapshot.GcGen2)); + } + } + + var totalRequests = slices.Sum(s => s.Requests); + var totalErrors = slices.Sum(s => s.Errors); + var durationSeconds = slices.Count > 0 ? slices.Count : 1; + + var allLatencies = new List(totalRequests); + lock (_lock) + { + foreach (var bucket in _latencyBuckets.Values) + { + allLatencies.AddRange(bucket); + } + } + allLatencies.Sort(); + + var summary = new StressSummary( + totalRequests, + totalErrors, + totalRequests / (double)durationSeconds, + allLatencies.Count > 0 ? Percentile(allLatencies, 0.50) : 0, + allLatencies.Count > 0 ? Percentile(allLatencies, 0.95) : 0, + allLatencies.Count > 0 ? Percentile(allLatencies, 0.99) : 0, + _memorySnapshots.Count > 0 ? _memorySnapshots.Max(m => m.MemoryBytes) : 0, + _memorySnapshots.Count > 0 ? _memorySnapshots[^1].MemoryBytes : 0, + totalRequests > 0 ? totalErrors / (double)totalRequests : 0); + + return new StressResult(server, config, slices, summary); + } + + private void SampleMemory(object? state) + { + var elapsed = Stopwatch.GetElapsedTime(_startTimestamp); + var second = (int)elapsed.TotalSeconds; + var memory = GC.GetTotalMemory(false); + var gcGen0 = GC.CollectionCount(0) - _gcGen0Baseline; + var gcGen1 = GC.CollectionCount(1) - _gcGen1Baseline; + var gcGen2 = GC.CollectionCount(2) - _gcGen2Baseline; + + lock (_lock) + { + _memorySnapshots.Add((second, memory, gcGen0, gcGen1, gcGen2)); + } + } + + private static double Percentile(List sorted, double percentile) + { + var index = (int)(sorted.Count * percentile); + if (index >= sorted.Count) + { + index = sorted.Count - 1; + } + return sorted[index]; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/Program.cs b/src/TurboHTTP.StressBenchmarks/Program.cs new file mode 100644 index 000000000..aa02f2c73 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Program.cs @@ -0,0 +1,151 @@ +using TurboHTTP.StressBenchmarks; +using TurboHTTP.StressBenchmarks.Reporting; +using TurboHTTP.StressBenchmarks.Scenarios; + +var scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) +{ + ["slow-handler"] = new SlowHandlerScenario(), + ["connection-storm"] = new ConnectionStormScenario(), + ["body-flood"] = new BodyFloodScenario(), + ["memory-endurance"] = new MemoryEnduranceScenario() +}; + +var scenarioName = "all"; +var serverFilter = "both"; +int? durationOverride = null; +int? concurrencyOverride = null; + +for (var i = 0; i < args.Length; i++) +{ + switch (args[i]) + { + case "--scenario" when i + 1 < args.Length: + scenarioName = args[++i]; + break; + case "--server" when i + 1 < args.Length: + serverFilter = args[++i]; + break; + case "--duration" when i + 1 < args.Length: + durationOverride = int.Parse(args[++i]); + break; + case "--concurrency" when i + 1 < args.Length: + concurrencyOverride = int.Parse(args[++i]); + break; + } +} + +ThreadPool.GetMinThreads(out var w, out var io); +ThreadPool.SetMinThreads(Math.Max(w, 1024), Math.Max(io, 1024)); + +Console.WriteLine("TurboHTTP Stress Benchmarks"); +Console.WriteLine(string.Concat("Scenario: ", scenarioName, " | Server: ", serverFilter)); +Console.WriteLine(); + +var toRun = scenarioName.Equals("all", StringComparison.OrdinalIgnoreCase) + ? scenarios.Values.ToList() + : scenarios.TryGetValue(scenarioName, out var s) + ? [s] + : throw new ArgumentException(string.Concat("Unknown scenario: ", scenarioName)); + +var runTurbo = serverFilter is "both" or "turbo"; +var runKestrel = serverFilter is "both" or "kestrel"; + +var comparisons = new List<(StressResult Turbo, StressResult Kestrel)>(); + +foreach (var scenario in toRun) +{ + var config = scenario.DefaultConfig; + if (durationOverride.HasValue) + { + config = config with { Duration = TimeSpan.FromSeconds(durationOverride.Value) }; + } + if (concurrencyOverride.HasValue) + { + config = config with { Concurrency = concurrencyOverride.Value }; + } + + Console.WriteLine(string.Concat("=== ", scenario.Name, " (", config.Concurrency.ToString(), " concurrent, ", ((int)config.Duration.TotalSeconds).ToString(), "s) ===")); + + StressResult? turboResult = null; + StressResult? kestrelResult = null; + + if (runTurbo) + { + turboResult = await RunScenario(scenario, config, ServerType.Turbo); + } + + if (runKestrel) + { + kestrelResult = await RunScenario(scenario, config, ServerType.Kestrel); + } + + if (turboResult is not null && kestrelResult is not null) + { + StressReport.PrintScenario(turboResult, kestrelResult); + comparisons.Add((turboResult, kestrelResult)); + } + else if (turboResult is not null) + { + PrintSingleResult(turboResult); + } + else if (kestrelResult is not null) + { + PrintSingleResult(kestrelResult); + } + + await JsonExporter.ExportAsync(scenario.Name, config, turboResult, kestrelResult); +} + +if (comparisons.Count > 1) +{ + StressReport.PrintSummary(comparisons); +} + +static async Task RunScenario(IStressScenario scenario, StressRunConfig config, ServerType serverType) +{ + Console.Write(string.Concat(" ", serverType.ToString(), ": starting server... ")); + + await using var harness = new ServerHarness(); + await harness.StartAsync(serverType, scenario.ConfigureRoutes); + + Console.Write("warmup... "); + var requestFunc = scenario.CreateRequestFunc(); + using var warmupCts = new CancellationTokenSource(config.WarmupDuration); + try + { + await LoadGenerator.RunAsync(harness.BaseUri!, config with { Concurrency = Math.Min(config.Concurrency, 10) }, requestFunc, _ => { }, warmupCts.Token); + } + catch (OperationCanceledException) + { + } + + GC.Collect(2, GCCollectionMode.Forced, true); + GC.WaitForPendingFinalizers(); + + Console.Write("load... "); + var collector = new MetricsCollector(); + using var loadCts = new CancellationTokenSource(config.Duration); + try + { + await LoadGenerator.RunAsync(harness.BaseUri!, config, requestFunc, collector.Record, loadCts.Token); + } + catch (OperationCanceledException) + { + } + + var result = collector.Build(serverType, config); + Console.WriteLine(string.Concat("done (", result.Summary.TotalRequests.ToString(), " requests, ", result.Summary.TotalErrors.ToString(), " errors)")); + + return result; +} + +static void PrintSingleResult(StressResult result) +{ + Console.WriteLine(); + Console.WriteLine(string.Concat("## ", result.Config.ScenarioName, " — ", result.Server.ToString())); + Console.WriteLine(string.Concat(" Throughput: ", result.Summary.AvgRps.ToString("F0"), " req/s")); + Console.WriteLine(string.Concat(" Latency p99: ", result.Summary.P99Ms.ToString("F1"), " ms")); + Console.WriteLine(string.Concat(" Peak Memory: ", (result.Summary.PeakMemoryBytes / (1024.0 * 1024.0)).ToString("F0"), " MB")); + Console.WriteLine(string.Concat(" Errors: ", result.Summary.TotalErrors.ToString())); + Console.WriteLine(); +} diff --git a/src/TurboHTTP.StressBenchmarks/Reporting/JsonExporter.cs b/src/TurboHTTP.StressBenchmarks/Reporting/JsonExporter.cs new file mode 100644 index 000000000..0c679ded4 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Reporting/JsonExporter.cs @@ -0,0 +1,117 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace TurboHTTP.StressBenchmarks.Reporting; + +public static class JsonExporter +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public static async Task ExportAsync(string scenarioName, StressRunConfig config, StressResult? turbo, StressResult? kestrel) + { + var outputDir = Path.Combine("results", "stress"); + Directory.CreateDirectory(outputDir); + + var report = new JsonReport + { + Scenario = scenarioName, + Config = new JsonConfig + { + Concurrency = config.Concurrency, + DurationSeconds = (int)config.Duration.TotalSeconds, + }, + Turbo = turbo is not null ? MapResult(turbo) : null, + Kestrel = kestrel is not null ? MapResult(kestrel) : null + }; + + var path = Path.Combine(outputDir, string.Concat(scenarioName, ".json")); + await using var stream = File.Create(path); + await JsonSerializer.SerializeAsync(stream, report, JsonOptions); + + Console.WriteLine(string.Concat("JSON exported to: ", path)); + } + + private static JsonServerResult MapResult(StressResult result) + { + return new JsonServerResult + { + TimeSeries = result.TimeSeries.Select(s => new JsonTimeSlice + { + T = s.Second, + Rps = s.Requests, + P50 = Math.Round(s.P50Ms, 1), + P95 = Math.Round(s.P95Ms, 1), + P99 = Math.Round(s.P99Ms, 1), + MemoryMb = Math.Round(s.MemoryBytes / (1024.0 * 1024.0), 1), + Errors = s.Errors, + GcGen0 = s.GcGen0, + GcGen1 = s.GcGen1, + GcGen2 = s.GcGen2 + }).ToList(), + Summary = new JsonSummary + { + TotalRequests = result.Summary.TotalRequests, + TotalErrors = result.Summary.TotalErrors, + AvgRps = Math.Round(result.Summary.AvgRps, 1), + P50Ms = Math.Round(result.Summary.P50Ms, 1), + P95Ms = Math.Round(result.Summary.P95Ms, 1), + P99Ms = Math.Round(result.Summary.P99Ms, 1), + PeakMemoryMb = Math.Round(result.Summary.PeakMemoryBytes / (1024.0 * 1024.0), 1), + FinalMemoryMb = Math.Round(result.Summary.FinalMemoryBytes / (1024.0 * 1024.0), 1), + ErrorRate = Math.Round(result.Summary.ErrorRate, 4) + } + }; + } + + private sealed class JsonReport + { + public string Scenario { get; set; } = ""; + public JsonConfig Config { get; set; } = new(); + public JsonServerResult? Turbo { get; set; } + public JsonServerResult? Kestrel { get; set; } + } + + private sealed class JsonConfig + { + public int Concurrency { get; set; } + public int DurationSeconds { get; set; } + } + + private sealed class JsonServerResult + { + public List TimeSeries { get; set; } = []; + public JsonSummary Summary { get; set; } = new(); + } + + private sealed class JsonTimeSlice + { + public int T { get; set; } + public int Rps { get; set; } + public double P50 { get; set; } + public double P95 { get; set; } + public double P99 { get; set; } + public double MemoryMb { get; set; } + public int Errors { get; set; } + public int GcGen0 { get; set; } + public int GcGen1 { get; set; } + public int GcGen2 { get; set; } + } + + private sealed class JsonSummary + { + public int TotalRequests { get; set; } + public int TotalErrors { get; set; } + public double AvgRps { get; set; } + public double P50Ms { get; set; } + public double P95Ms { get; set; } + public double P99Ms { get; set; } + public double PeakMemoryMb { get; set; } + public double FinalMemoryMb { get; set; } + public double ErrorRate { get; set; } + } +} diff --git a/src/TurboHTTP.StressBenchmarks/Reporting/StressReport.cs b/src/TurboHTTP.StressBenchmarks/Reporting/StressReport.cs new file mode 100644 index 000000000..2f056be0a --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Reporting/StressReport.cs @@ -0,0 +1,91 @@ +using System.Text; + +namespace TurboHTTP.StressBenchmarks.Reporting; + +public static class StressReport +{ + public static void PrintScenario(StressResult turbo, StressResult kestrel) + { + var config = turbo.Config; + Console.WriteLine(); + Console.WriteLine(string.Concat("## ", config.ScenarioName, " (", config.Concurrency.ToString(), " concurrent, ", ((int)config.Duration.TotalSeconds).ToString(), "s)")); + Console.WriteLine(); + Console.WriteLine("| Metric | TurboServer | Kestrel | Delta |"); + Console.WriteLine("|-----------------------|-------------|-----------|---------|"); + + PrintRow("Throughput (req/s)", turbo.Summary.AvgRps, kestrel.Summary.AvgRps, "F0", higherIsBetter: true); + PrintRow("Latency p50 (ms)", turbo.Summary.P50Ms, kestrel.Summary.P50Ms, "F1", higherIsBetter: false); + PrintRow("Latency p95 (ms)", turbo.Summary.P95Ms, kestrel.Summary.P95Ms, "F1", higherIsBetter: false); + PrintRow("Latency p99 (ms)", turbo.Summary.P99Ms, kestrel.Summary.P99Ms, "F1", higherIsBetter: false); + PrintRow("Peak Memory (MB)", turbo.Summary.PeakMemoryBytes / (1024.0 * 1024.0), kestrel.Summary.PeakMemoryBytes / (1024.0 * 1024.0), "F0", higherIsBetter: false); + PrintCountRow("Errors", turbo.Summary.TotalErrors, kestrel.Summary.TotalErrors); + PrintRow("Error Rate", turbo.Summary.ErrorRate * 100, kestrel.Summary.ErrorRate * 100, "F1", higherIsBetter: false, suffix: "%"); + + Console.WriteLine(); + } + + public static void PrintSummary(IReadOnlyList<(StressResult Turbo, StressResult Kestrel)> results) + { + Console.WriteLine("## Summary"); + Console.WriteLine(); + Console.WriteLine("| Scenario | Winner | Key Advantage |"); + Console.WriteLine("|------------------|-------------|----------------------------------|"); + + foreach (var (turbo, kestrel) in results) + { + var name = turbo.Config.ScenarioName; + var turboScore = 0; + var kestrelScore = 0; + + if (turbo.Summary.P99Ms < kestrel.Summary.P99Ms) turboScore++; else kestrelScore++; + if (turbo.Summary.PeakMemoryBytes < kestrel.Summary.PeakMemoryBytes) turboScore++; else kestrelScore++; + if (turbo.Summary.TotalErrors < kestrel.Summary.TotalErrors) turboScore++; else kestrelScore++; + + var winner = turboScore >= kestrelScore ? "TurboServer" : "Kestrel"; + var advantage = BuildAdvantage(turbo, kestrel); + + Console.WriteLine(string.Concat("| ", name.PadRight(16), " | ", winner.PadRight(11), " | ", advantage.PadRight(32), " |")); + } + + Console.WriteLine(); + } + + private static void PrintRow(string metric, double turbo, double kestrel, string format, bool higherIsBetter, string suffix = "") + { + var turboStr = string.Concat(turbo.ToString(format), suffix); + var kestrelStr = string.Concat(kestrel.ToString(format), suffix); + var delta = kestrel != 0 ? ((turbo - kestrel) / kestrel) * 100 : 0; + var deltaStr = string.Concat(delta >= 0 ? "+" : "", delta.ToString("F1"), "%"); + + Console.WriteLine(string.Concat("| ", metric.PadRight(21), " | ", turboStr.PadRight(11), " | ", kestrelStr.PadRight(9), " | ", deltaStr.PadRight(7), " |")); + } + + private static void PrintCountRow(string metric, int turbo, int kestrel) + { + Console.WriteLine(string.Concat("| ", metric.PadRight(21), " | ", turbo.ToString().PadRight(11), " | ", kestrel.ToString().PadRight(9), " | — |")); + } + + private static string BuildAdvantage(StressResult turbo, StressResult kestrel) + { + var parts = new List(3); + + if (kestrel.Summary.P99Ms > 0 && turbo.Summary.P99Ms < kestrel.Summary.P99Ms) + { + var pct = ((kestrel.Summary.P99Ms - turbo.Summary.P99Ms) / kestrel.Summary.P99Ms * 100).ToString("F0"); + parts.Add(string.Concat("p99 ", pct, "% lower")); + } + + if (kestrel.Summary.PeakMemoryBytes > 0 && turbo.Summary.PeakMemoryBytes < kestrel.Summary.PeakMemoryBytes) + { + var pct = ((kestrel.Summary.PeakMemoryBytes - turbo.Summary.PeakMemoryBytes) / (double)kestrel.Summary.PeakMemoryBytes * 100).ToString("F0"); + parts.Add(string.Concat(pct, "% less memory")); + } + + if (turbo.Summary.TotalErrors == 0 && kestrel.Summary.TotalErrors > 0) + { + parts.Add("zero errors"); + } + + return parts.Count > 0 ? string.Join(", ", parts) : "comparable"; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/RequestResult.cs b/src/TurboHTTP.StressBenchmarks/RequestResult.cs new file mode 100644 index 000000000..5ebedcaaf --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/RequestResult.cs @@ -0,0 +1,3 @@ +namespace TurboHTTP.StressBenchmarks; + +public sealed record RequestResult(int StatusCode, double ElapsedMs, Exception? Error); diff --git a/src/TurboHTTP.StressBenchmarks/Scenarios/BodyFloodScenario.cs b/src/TurboHTTP.StressBenchmarks/Scenarios/BodyFloodScenario.cs new file mode 100644 index 000000000..6b98568eb --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Scenarios/BodyFloodScenario.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace TurboHTTP.StressBenchmarks.Scenarios; + +public sealed class BodyFloodScenario : IStressScenario +{ + private static readonly byte[] Payload = GeneratePayload(1 * 1024 * 1024); + + public string Name => "body-flood"; + + public StressRunConfig DefaultConfig => new( + Name, + Concurrency: 200, + Duration: TimeSpan.FromSeconds(30), + WarmupDuration: TimeSpan.FromSeconds(5), + RequestBodySize: 1 * 1024 * 1024, + DisableKeepAlive: false); + + public void ConfigureRoutes(WebApplication app) + { + app.MapPost("/stress", async ctx => + { + long count = 0; + var buffer = new byte[64 * 1024]; + int read; + while ((read = await ctx.Request.Body.ReadAsync(buffer)) > 0) + { + count += read; + } + ctx.Response.ContentType = "text/plain"; + await ctx.Response.WriteAsync(string.Concat("received:", count.ToString())); + }); + } + + public Func> CreateRequestFunc() + { + return static (client, baseUri) => + { + var uri = new Uri(baseUri, "/stress"); + var content = new ByteArrayContent(Payload); + return client.PostAsync(uri, content); + }; + } + + private static byte[] GeneratePayload(int sizeBytes) + { + var payload = new byte[sizeBytes]; + for (var i = 0; i < sizeBytes; i++) + { + payload[i] = (byte)(i % 256); + } + return payload; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/Scenarios/ConnectionStormScenario.cs b/src/TurboHTTP.StressBenchmarks/Scenarios/ConnectionStormScenario.cs new file mode 100644 index 000000000..10169b292 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Scenarios/ConnectionStormScenario.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace TurboHTTP.StressBenchmarks.Scenarios; + +public sealed class ConnectionStormScenario : IStressScenario +{ + public string Name => "connection-storm"; + + public StressRunConfig DefaultConfig => new( + Name, + Concurrency: 200, + Duration: TimeSpan.FromSeconds(30), + WarmupDuration: TimeSpan.FromSeconds(5), + RequestBodySize: null, + DisableKeepAlive: true); + + public void ConfigureRoutes(WebApplication app) + { + app.MapGet("/stress", () => Results.Content("OK", "text/plain")); + } + + public Func> CreateRequestFunc() + { + return static (client, baseUri) => + { + var uri = new Uri(baseUri, "/stress"); + return client.GetAsync(uri); + }; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/Scenarios/MemoryEnduranceScenario.cs b/src/TurboHTTP.StressBenchmarks/Scenarios/MemoryEnduranceScenario.cs new file mode 100644 index 000000000..00f2188b7 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Scenarios/MemoryEnduranceScenario.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace TurboHTTP.StressBenchmarks.Scenarios; + +public sealed class MemoryEnduranceScenario : IStressScenario +{ + public string Name => "memory-endurance"; + + public StressRunConfig DefaultConfig => new( + Name, + Concurrency: 100, + Duration: TimeSpan.FromSeconds(120), + WarmupDuration: TimeSpan.FromSeconds(5), + RequestBodySize: null, + DisableKeepAlive: false); + + public void ConfigureRoutes(WebApplication app) + { + app.MapGet("/stress", async () => + { + await Task.Delay(10); + return Results.Json(new { message = "Hello, World!", timestamp = DateTime.UtcNow }); + }); + } + + public Func> CreateRequestFunc() + { + return static (client, baseUri) => + { + var uri = new Uri(baseUri, "/stress"); + return client.GetAsync(uri); + }; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/Scenarios/SlowHandlerScenario.cs b/src/TurboHTTP.StressBenchmarks/Scenarios/SlowHandlerScenario.cs new file mode 100644 index 000000000..413b96d3e --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/Scenarios/SlowHandlerScenario.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace TurboHTTP.StressBenchmarks.Scenarios; + +public sealed class SlowHandlerScenario : IStressScenario +{ + public string Name => "slow-handler"; + + public StressRunConfig DefaultConfig => new( + Name, + Concurrency: 500, + Duration: TimeSpan.FromSeconds(30), + WarmupDuration: TimeSpan.FromSeconds(5), + RequestBodySize: null, + DisableKeepAlive: false); + + public void ConfigureRoutes(WebApplication app) + { + app.MapGet("/stress", async () => + { + await Task.Delay(2000); + return Results.Content("OK", "text/plain"); + }); + } + + public Func> CreateRequestFunc() + { + return static (client, baseUri) => + { + var uri = new Uri(baseUri, "/stress"); + return client.GetAsync(uri); + }; + } +} diff --git a/src/TurboHTTP.StressBenchmarks/ServerHarness.cs b/src/TurboHTTP.StressBenchmarks/ServerHarness.cs new file mode 100644 index 000000000..1d9218c39 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/ServerHarness.cs @@ -0,0 +1,61 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TurboHTTP.Server; + +namespace TurboHTTP.StressBenchmarks; + +public sealed class ServerHarness : IAsyncDisposable +{ + private WebApplication? _app; + + public Uri? BaseUri { get; private set; } + + public async Task StartAsync(ServerType serverType, Action configureRoutes) + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + if (serverType == ServerType.Turbo) + { + builder.Host.UseTurboHttp(options => + { + options.Listen(IPAddress.Loopback, 0, lo => + lo.Protocols = HttpProtocols.Http1); + }); + } + else + { + builder.WebHost.ConfigureKestrel((context, options) => + { + options.Listen(IPAddress.Loopback, 0); + }); + } + + var app = builder.Build(); + configureRoutes(app); + + await app.StartAsync(); + + var addresses = app.Services.GetRequiredService() + .Features.Get()! + .Addresses + .ToArray(); + + BaseUri = new Uri(addresses[0]); + _app = app; + } + + public async ValueTask DisposeAsync() + { + if (_app is not null) + { + await _app.StopAsync(); + await _app.DisposeAsync(); + } + } +} diff --git a/src/TurboHTTP.StressBenchmarks/ServerType.cs b/src/TurboHTTP.StressBenchmarks/ServerType.cs new file mode 100644 index 000000000..bc6770c49 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/ServerType.cs @@ -0,0 +1,7 @@ +namespace TurboHTTP.StressBenchmarks; + +public enum ServerType +{ + Turbo, + Kestrel +} diff --git a/src/TurboHTTP.StressBenchmarks/StressResult.cs b/src/TurboHTTP.StressBenchmarks/StressResult.cs new file mode 100644 index 000000000..a9d66916b --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/StressResult.cs @@ -0,0 +1,7 @@ +namespace TurboHTTP.StressBenchmarks; + +public sealed record StressResult( + ServerType Server, + StressRunConfig Config, + IReadOnlyList TimeSeries, + StressSummary Summary); diff --git a/src/TurboHTTP.StressBenchmarks/StressRunConfig.cs b/src/TurboHTTP.StressBenchmarks/StressRunConfig.cs new file mode 100644 index 000000000..21ce71629 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/StressRunConfig.cs @@ -0,0 +1,9 @@ +namespace TurboHTTP.StressBenchmarks; + +public sealed record StressRunConfig( + string ScenarioName, + int Concurrency, + TimeSpan Duration, + TimeSpan WarmupDuration, + int? RequestBodySize, + bool DisableKeepAlive); diff --git a/src/TurboHTTP.StressBenchmarks/StressSummary.cs b/src/TurboHTTP.StressBenchmarks/StressSummary.cs new file mode 100644 index 000000000..d406f6c28 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/StressSummary.cs @@ -0,0 +1,12 @@ +namespace TurboHTTP.StressBenchmarks; + +public sealed record StressSummary( + int TotalRequests, + int TotalErrors, + double AvgRps, + double P50Ms, + double P95Ms, + double P99Ms, + long PeakMemoryBytes, + long FinalMemoryBytes, + double ErrorRate); diff --git a/src/TurboHTTP.StressBenchmarks/TimeSlice.cs b/src/TurboHTTP.StressBenchmarks/TimeSlice.cs new file mode 100644 index 000000000..a88c4d3e7 --- /dev/null +++ b/src/TurboHTTP.StressBenchmarks/TimeSlice.cs @@ -0,0 +1,13 @@ +namespace TurboHTTP.StressBenchmarks; + +public sealed record TimeSlice( + int Second, + int Requests, + int Errors, + double P50Ms, + double P95Ms, + double P99Ms, + long MemoryBytes, + int GcGen0, + int GcGen1, + int GcGen2); diff --git a/src/TurboHTTP.MicroBenchmarks/TurboHTTP.MicroBenchmarks.csproj b/src/TurboHTTP.StressBenchmarks/TurboHTTP.StressBenchmarks.csproj similarity index 55% rename from src/TurboHTTP.MicroBenchmarks/TurboHTTP.MicroBenchmarks.csproj rename to src/TurboHTTP.StressBenchmarks/TurboHTTP.StressBenchmarks.csproj index 6e02b545f..2548675a1 100644 --- a/src/TurboHTTP.MicroBenchmarks/TurboHTTP.MicroBenchmarks.csproj +++ b/src/TurboHTTP.StressBenchmarks/TurboHTTP.StressBenchmarks.csproj @@ -2,18 +2,15 @@ Exe - true - true false - + - - + diff --git a/src/TurboHTTP.Tests.Shared/ActorSystemFixture.cs b/src/TurboHTTP.Tests.Shared/ActorSystemFixture.cs index c95bd51f9..d9e89b9f4 100644 --- a/src/TurboHTTP.Tests.Shared/ActorSystemFixture.cs +++ b/src/TurboHTTP.Tests.Shared/ActorSystemFixture.cs @@ -11,8 +11,7 @@ namespace TurboHTTP.Tests.Shared; public sealed class ActorSystemFixture : IAsyncLifetime { - private static readonly Config QuietConfig = ConfigurationFactory.ParseString( - "akka.loglevel = WARNING"); + private static readonly Config QuietConfig = ConfigurationFactory.ParseString("akka.loglevel = WARNING"); public ActorSystem System { get; private set; } = null!; diff --git a/src/TurboHTTP.Tests.Shared/ClientAcceptanceTestBase.cs b/src/TurboHTTP.Tests.Shared/ClientAcceptanceTestBase.cs index 35b0092f0..c1e915036 100644 --- a/src/TurboHTTP.Tests.Shared/ClientAcceptanceTestBase.cs +++ b/src/TurboHTTP.Tests.Shared/ClientAcceptanceTestBase.cs @@ -1,5 +1,4 @@ using TurboHTTP.Client; -using System.Net; using TurboHTTP.Streams; using Xunit; @@ -7,7 +6,7 @@ namespace TurboHTTP.Tests.Shared; public abstract class ClientAcceptanceTestBase : AcceptanceTestBase { - protected async Task SendClientAsync( + protected static async Task SendClientAsync( Version version, HttpRequestMessage request, Func responseFactory, diff --git a/src/TurboHTTP.Tests.Shared/EngineTestBase.cs b/src/TurboHTTP.Tests.Shared/EngineTestBase.cs index 48e6d13e6..5ad206509 100644 --- a/src/TurboHTTP.Tests.Shared/EngineTestBase.cs +++ b/src/TurboHTTP.Tests.Shared/EngineTestBase.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text; using Akka; using Akka.Streams.Dsl; @@ -408,7 +409,7 @@ private static List DrainOutboundBytes(TestConnectionStage stage, bool str var preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"u8; var bytes = new List(); var prefaceStripped = false; - int messageCount = 0; + var messageCount = 0; while (stage.TryGetOutbound(out var outbound)) { @@ -437,7 +438,7 @@ private static List DrainOutboundBytes(TestConnectionStage stage, bool str bytes.AddRange(span.ToArray()); } - System.Diagnostics.Debug.WriteLine($"DrainOutboundBytes: {messageCount} outbound messages, {bytes.Count} total bytes"); + Debug.WriteLine($"DrainOutboundBytes: {messageCount} outbound messages, {bytes.Count} total bytes"); return bytes; } diff --git a/src/TurboHTTP.Tests.Shared/FakeServerOps.cs b/src/TurboHTTP.Tests.Shared/FakeServerOps.cs index d19803a74..f72b3dd31 100644 --- a/src/TurboHTTP.Tests.Shared/FakeServerOps.cs +++ b/src/TurboHTTP.Tests.Shared/FakeServerOps.cs @@ -3,23 +3,20 @@ using Akka.Streams; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context; -using TurboHTTP.Server; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Server; namespace TurboHTTP.Tests.Shared; internal sealed class FakeServerOps : IServerStageOperations { - private readonly List _contexts = []; + private readonly List _features = []; - public List Requests => _contexts; + public List Requests => _features; public List Outbound { get; } = []; public List<(string Name, TimeSpan Delay)> ScheduledTimers { get; } = []; public List CancelledTimers { get; } = []; - public void OnRequest(TurboHttpContext context) => _contexts.Add(context); + public void OnRequest(IFeatureCollection features) => _features.Add(features); public void OnOutbound(ITransportOutbound item) => Outbound.Add(item); public void OnScheduleTimer(string name, TimeSpan delay) diff --git a/src/TurboHTTP.Tests.Shared/H2ResponseBuilder.cs b/src/TurboHTTP.Tests.Shared/H2ResponseBuilder.cs index ae4c6a86a..38c451e50 100644 --- a/src/TurboHTTP.Tests.Shared/H2ResponseBuilder.cs +++ b/src/TurboHTTP.Tests.Shared/H2ResponseBuilder.cs @@ -84,16 +84,6 @@ public H2ResponseBuilder WindowUpdate(int streamId, int increment) return this; } - /// - /// Appends a PING frame (8 bytes of opaque data). - /// - public H2ResponseBuilder Ping(ReadOnlyMemory? data = null, bool isAck = false) - { - var pingData = data ?? new byte[8]; - _frames.Add(new PingFrame(pingData, isAck: isAck)); - return this; - } - /// /// Appends a GOAWAY frame on stream 0. /// diff --git a/src/TurboHTTP.Tests.Shared/ProtocolVariant.cs b/src/TurboHTTP.Tests.Shared/ProtocolVariant.cs index 22e58aaa2..d1ef18ba1 100644 --- a/src/TurboHTTP.Tests.Shared/ProtocolVariant.cs +++ b/src/TurboHTTP.Tests.Shared/ProtocolVariant.cs @@ -12,10 +12,10 @@ public ProtocolVariant() { } - public ProtocolVariant(TestHttpVersion Version, bool Tls) + public ProtocolVariant(TestHttpVersion version, bool tls) { - this.Version = Version; - this.Tls = Tls; + Version = version; + Tls = tls; } public void Serialize(IXunitSerializationInfo info) diff --git a/src/TurboHTTP.Tests.Shared/ServerTestContext.cs b/src/TurboHTTP.Tests.Shared/ServerTestContext.cs index 581052d4f..77f8f33b5 100644 --- a/src/TurboHTTP.Tests.Shared/ServerTestContext.cs +++ b/src/TurboHTTP.Tests.Shared/ServerTestContext.cs @@ -1,14 +1,11 @@ using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Tests.Shared; internal static class ServerTestContext { - internal static ServerTestContextBuilder Request() => new(); - - internal static TurboHttpContext CreateResponse(int statusCode = 200) + internal static IFeatureCollection CreateResponse(int statusCode = 200) { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); @@ -16,21 +13,13 @@ internal static TurboHttpContext CreateResponse(int statusCode = 200) features.Set(responseFeature); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); - } - - internal static TurboHttpContext CreateH2Response(int streamId, int statusCode = 200) - { - var ctx = CreateResponse(statusCode); - ctx.Features.Set(new TurboStreamIdFeature(streamId)); - return ctx; + return features; } - internal static TurboHttpContext CreateH3Response(long streamId, int statusCode = 200) + internal static IFeatureCollection CreateH3Response(long streamId, int statusCode = 200) { - var ctx = CreateResponse(statusCode); - ctx.Features.Set(new TurboStreamIdFeature(streamId)); - return ctx; + var features = CreateResponse(statusCode); + features.Set(new TurboStreamIdFeature(streamId)); + return features; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests.Shared/ServerTestContextBuilder.cs b/src/TurboHTTP.Tests.Shared/ServerTestContextBuilder.cs deleted file mode 100644 index fc5fc0538..000000000 --- a/src/TurboHTTP.Tests.Shared/ServerTestContextBuilder.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Text; -using Akka; -using Akka.Streams; -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Shared; - -internal sealed class ServerTestContextBuilder -{ - private string _method = "GET"; - private string _scheme = "http"; - private string _host = "localhost"; - private string _path = "/"; - private string _queryString = ""; - private string _protocol = "HTTP/1.1"; - private readonly HeaderDictionary _headers = new(); - private Stream _body = Stream.Null; - private Source, NotUsed>? _bodySource; - private TurboConnectionInfo? _connection; - private IServiceProvider? _services; - private CancellationToken _cancellationToken; - private IMaterializer? _materializer; - - public ServerTestContextBuilder Get(string path) => Method("GET").Path(path); - public ServerTestContextBuilder Post(string path) => Method("POST").Path(path); - public ServerTestContextBuilder Put(string path) => Method("PUT").Path(path); - public ServerTestContextBuilder Delete(string path) => Method("DELETE").Path(path); - - public ServerTestContextBuilder Method(string method) - { - _method = method; - return this; - } - - public ServerTestContextBuilder Path(string path) - { - var qIndex = path.IndexOf('?'); - if (qIndex >= 0) - { - _path = path[..qIndex]; - _queryString = path[qIndex..]; - } - else - { - _path = path; - _queryString = ""; - } - - return this; - } - - public ServerTestContextBuilder Scheme(string scheme) - { - _scheme = scheme; - return this; - } - - public ServerTestContextBuilder Host(string host) - { - _host = host; - return this; - } - - public ServerTestContextBuilder Protocol(string protocol) - { - _protocol = protocol; - return this; - } - - public ServerTestContextBuilder Header(string name, string value) - { - _headers[name] = value; - return this; - } - - public ServerTestContextBuilder Body(Stream body) - { - _body = body; - return this; - } - - public ServerTestContextBuilder Body(byte[] data) - { - _body = new MemoryStream(data); - return this; - } - - public ServerTestContextBuilder BodySource(Source, NotUsed> source) - { - _bodySource = source; - return this; - } - - public ServerTestContextBuilder FormBody(string urlEncodedData) - { - _headers["Content-Type"] = "application/x-www-form-urlencoded"; - _body = new MemoryStream(Encoding.UTF8.GetBytes(urlEncodedData)); - return this; - } - - public ServerTestContextBuilder MultipartBody(Action configure) - { - var content = new MultipartFormDataContent(); - configure(content); - var stream = new MemoryStream(); - content.CopyTo(stream, null, CancellationToken.None); - stream.Position = 0; - _body = stream; - _headers["Content-Type"] = content.Headers.ContentType!.ToString(); - return this; - } - - public ServerTestContextBuilder JsonBody(string json) - { - _headers["Content-Type"] = "application/json"; - _body = new MemoryStream(Encoding.UTF8.GetBytes(json)); - return this; - } - - public ServerTestContextBuilder Connection(TurboConnectionInfo connection) - { - _connection = connection; - return this; - } - - public ServerTestContextBuilder Services(IServiceProvider services) - { - _services = services; - return this; - } - - public ServerTestContextBuilder RequestAborted(CancellationToken token) - { - _cancellationToken = token; - return this; - } - - public ServerTestContextBuilder Materializer(IMaterializer materializer) - { - _materializer = materializer; - return this; - } - - public TurboHttpRequestFeature BuildRequestFeature() - { - var rawTarget = string.IsNullOrEmpty(_queryString) - ? _path - : string.Concat(_path, _queryString); - - return new TurboHttpRequestFeature - { - Method = _method, - Scheme = _scheme, - Path = _path, - QueryString = _queryString, - RawTarget = rawTarget, - Protocol = _protocol, - Headers = _headers, - Body = _body, - ExtractedHost = _host - }; - } - - public TurboHttpContext Build() - { - var conn = _connection ?? new TurboConnectionInfo("test", null, 0, null, 0); - - var features = new TurboFeatureCollection(); - var requestFeature = BuildRequestFeature(); - features.Set(requestFeature); - var requestBodyFeature = new TurboRequestBodyFeature - { - Body = requestFeature.Body, - BodySource = _bodySource ?? Source.Empty>() - }; - features.Set(requestBodyFeature); - features.Set(new TurboHttpResponseFeature()); - features.Set(new TurboHttpConnectionFeature(conn)); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - - return new TurboHttpContext(features, conn, _services, _cancellationToken, _materializer!); - } -} diff --git a/src/TurboHTTP.Tests.Shared/TurboServerFixture.cs b/src/TurboHTTP.Tests.Shared/TurboServerFixture.cs deleted file mode 100644 index 566f6323b..000000000 --- a/src/TurboHTTP.Tests.Shared/TurboServerFixture.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging; -using Servus.Akka.Transport; -using TurboHTTP.Server; -using Xunit; - -namespace TurboHTTP.Tests.Shared; - -public class TurboServerFixture : IAsyncLifetime -{ - private readonly Action _configureRoutes; - private TurboWebApplication? _app; - - public TurboServerFixture(Action configureRoutes) - { - _configureRoutes = configureRoutes; - } - - public Uri HttpBaseAddress { get; private set; } = null!; - public int HttpPort { get; private set; } - - public async ValueTask InitializeAsync() - { - var port = GetFreePort(); - var builder = TurboWebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - - builder.Server.Bind(new TcpListenerOptions { Host = "127.0.0.1", Port = (ushort)port }); - - _app = builder.Build(); - _configureRoutes(_app); - - await _app.StartAsync(); - - HttpPort = port; - HttpBaseAddress = new Uri($"http://127.0.0.1:{port}"); - } - - public async ValueTask DisposeAsync() - { - if (_app is not null) - { - await _app.StopAsync(); - await _app.DisposeAsync(); - } - } - - private static int GetFreePort() - { - using var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } -} diff --git a/src/TurboHTTP.Tests/Client/ExtensionsSpec.cs b/src/TurboHTTP.Tests/Client/ExtensionsSpec.cs index 737732bc3..9b4d138d6 100644 --- a/src/TurboHTTP.Tests/Client/ExtensionsSpec.cs +++ b/src/TurboHTTP.Tests/Client/ExtensionsSpec.cs @@ -4,7 +4,7 @@ using Akka.Streams.Dsl; using Akka.TestKit.Xunit; using TurboHTTP.Client; -using TurboHTTP.Features.Sse; +using Servus.Akka.Sse; using TurboHTTP.Internal; namespace TurboHTTP.Tests.Client; diff --git a/src/TurboHTTP.Tests/Context/AdapterDualImplSpec.cs b/src/TurboHTTP.Tests/Context/AdapterDualImplSpec.cs deleted file mode 100644 index 988e296d2..000000000 --- a/src/TurboHTTP.Tests/Context/AdapterDualImplSpec.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Context; -using TurboHTTP.Context.Adapters; - -namespace TurboHTTP.Tests.Context; - -public sealed class AdapterDualImplSpec -{ - [Fact(Timeout = 5000)] - public void TurboResponseHeaderDictionary_should_implement_ITurboHeaderDictionary() - { - var headers = new TurboResponseHeaderDictionary(); - Assert.IsAssignableFrom(headers); - } - - [Fact(Timeout = 5000)] - public void TurboResponseHeaderDictionary_should_implement_IHeaderDictionary() - { - var headers = new TurboResponseHeaderDictionary(); - Assert.IsAssignableFrom(headers); - } - - [Fact(Timeout = 5000)] - public void ITurboHeaderDictionary_should_expose_same_values_as_IHeaderDictionary() - { - var headers = new TurboResponseHeaderDictionary(); - headers["Content-Type"] = "text/plain"; - - var turbo = (ITurboHeaderDictionary)headers; - var aspnet = (IHeaderDictionary)headers; - - Assert.Equal("text/plain", turbo["Content-Type"].ToString()); - Assert.Equal(aspnet["Content-Type"], turbo["Content-Type"]); - } - - [Fact(Timeout = 5000)] - public void TurboQueryCollection_should_implement_ITurboQueryCollection() - { - var query = new TurboQueryCollection("?key=value"); - Assert.IsAssignableFrom(query); - } - - [Fact(Timeout = 5000)] - public void ITurboQueryCollection_indexer_should_return_first_value() - { - var query = new TurboQueryCollection("?key=value"); - var turbo = (ITurboQueryCollection)query; - - Assert.Equal("value", turbo["key"]); - Assert.Equal(1, turbo.Count); - Assert.True(turbo.ContainsKey("key")); - } - - [Fact(Timeout = 5000)] - public void TurboRequestCookieCollection_should_implement_ITurboRequestCookieCollection() - { - var cookies = new TurboRequestCookieCollection("session=abc"); - Assert.IsAssignableFrom(cookies); - } - - [Fact(Timeout = 5000)] - public void ITurboRequestCookieCollection_indexer_should_return_value() - { - var cookies = new TurboRequestCookieCollection("session=abc; theme=dark"); - var turbo = (ITurboRequestCookieCollection)cookies; - - Assert.Equal("abc", turbo["session"]); - Assert.Equal("dark", turbo["theme"]); - Assert.Equal(2, turbo.Count); - Assert.True(turbo.ContainsKey("session")); - } - - [Fact(Timeout = 5000)] - public void TurboFormFile_should_implement_ITurboFormFile() - { - var file = new TurboFormFile("file", "test.txt", "text/plain", [1, 2, 3]); - Assert.IsAssignableFrom(file); - } - - [Fact(Timeout = 5000)] - public void ITurboFormFile_should_expose_properties() - { - var file = new TurboFormFile("file", "test.txt", "text/plain", [1, 2, 3]); - var turbo = (ITurboFormFile)file; - - Assert.Equal("file", turbo.Name); - Assert.Equal("test.txt", turbo.FileName); - Assert.Equal(3, turbo.Length); - Assert.NotNull(turbo.OpenReadStream()); - } - - [Fact(Timeout = 5000)] - public void TurboFormCollection_should_implement_ITurboFormCollection() - { - var fields = new Dictionary - { - ["name"] = "test" - }; - var files = new TurboFormFileCollection([]); - var form = new TurboFormCollection(fields, files); - Assert.IsAssignableFrom(form); - } -} diff --git a/src/TurboHTTP.Tests/Context/Features/FeatureInterfaceDualImplSpec.cs b/src/TurboHTTP.Tests/Context/Features/FeatureInterfaceDualImplSpec.cs deleted file mode 100644 index c8ab08b7b..000000000 --- a/src/TurboHTTP.Tests/Context/Features/FeatureInterfaceDualImplSpec.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Context.Features; - -public sealed class FeatureInterfaceDualImplSpec -{ - [Fact(Timeout = 5000)] - public void TurboHttpRequestFeature_should_implement_both_interfaces() - { - var feature = new TurboHttpRequestFeature(); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboHttpResponseFeature_should_implement_both_interfaces() - { - var feature = new TurboHttpResponseFeature(); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboHttpConnectionFeature_should_implement_both_interfaces() - { - var info = new TurboConnectionInfo("test-id", null, 0, null, 0); - var feature = new TurboHttpConnectionFeature(info); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboHttpRequestBodyDetectionFeature_should_implement_both_interfaces() - { - var feature = new TurboHttpRequestBodyDetectionFeature(true); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboHttpResponseTrailersFeature_should_implement_both_interfaces() - { - var feature = new TurboHttpResponseTrailersFeature(); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboHttpResetFeature_should_implement_both_interfaces() - { - var feature = new TurboHttpResetFeature(_ => { }); - Assert.IsAssignableFrom(feature); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void Request_feature_should_share_state_across_interfaces() - { - var feature = new TurboHttpRequestFeature(); - ((IHttpRequestFeature)feature).Method = "POST"; - Assert.Equal("POST", ((ITurboRequestFeature)feature).Method); - } - - [Fact(Timeout = 5000)] - public void Response_feature_should_share_state_across_interfaces() - { - var feature = new TurboHttpResponseFeature(); - ((IHttpResponseFeature)feature).StatusCode = 404; - Assert.Equal(404, ((ITurboResponseFeature)feature).StatusCode); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboHttpConnectionFeatureSpec.cs b/src/TurboHTTP.Tests/Context/TurboHttpConnectionFeatureSpec.cs deleted file mode 100644 index 29c857488..000000000 --- a/src/TurboHTTP.Tests/Context/TurboHttpConnectionFeatureSpec.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Net; -using TurboHTTP.Server; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboHttpConnectionFeatureSpec -{ - [Fact(Timeout = 5000)] - public void ConnectionId_should_delegate_to_connection_info() - { - var info = new TurboConnectionInfo("conn-42", IPAddress.Loopback, 12345, IPAddress.Any, 443); - var feature = new TurboHttpConnectionFeature(info); - Assert.Equal("conn-42", feature.ConnectionId); - } - - [Fact(Timeout = 5000)] - public void RemoteIpAddress_should_delegate_to_connection_info() - { - var info = new TurboConnectionInfo("c", IPAddress.Parse("10.0.0.1"), 9999, IPAddress.Any, 443); - var feature = new TurboHttpConnectionFeature(info); - Assert.Equal(IPAddress.Parse("10.0.0.1"), feature.RemoteIpAddress); - Assert.Equal(9999, feature.RemotePort); - } - - [Fact(Timeout = 5000)] - public void LocalEndpoint_should_delegate_to_connection_info() - { - var info = new TurboConnectionInfo("c", IPAddress.Loopback, 0, IPAddress.Parse("192.168.1.1"), 8080); - var feature = new TurboHttpConnectionFeature(info); - Assert.Equal(IPAddress.Parse("192.168.1.1"), feature.LocalIpAddress); - Assert.Equal(8080, feature.LocalPort); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboHttpRequestSpec.cs b/src/TurboHTTP.Tests/Context/TurboHttpRequestSpec.cs deleted file mode 100644 index 89a767aa1..000000000 --- a/src/TurboHTTP.Tests/Context/TurboHttpRequestSpec.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Features; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboHttpRequestSpec -{ - [Fact(Timeout = 5000)] - public void Method_should_delegate_to_feature() - { - var (request, _) = CreateRequest("POST", "/test"); - Assert.Equal("POST", request.Method); - } - - [Fact(Timeout = 5000)] - public void Path_should_delegate_to_feature() - { - var (request, _) = CreateRequest("GET", "/api/users"); - Assert.Equal("/api/users", request.Path.Value); - } - - [Fact(Timeout = 5000)] - public void QueryString_should_delegate_to_feature() - { - var (request, _) = CreateRequest("GET", "/test?page=1"); - Assert.Equal("?page=1", request.QueryString.Value); - } - - [Fact(Timeout = 5000)] - public void Scheme_should_delegate_to_feature() - { - var (request, _) = CreateRequest("GET", "/test", scheme: "https"); - Assert.Equal("https", request.Scheme); - } - - [Fact(Timeout = 5000)] - public void Protocol_should_delegate_to_feature() - { - var (request, _) = CreateRequest("GET", "/test"); - Assert.Equal("HTTP/1.1", request.Protocol); - } - - [Fact(Timeout = 5000)] - public void Headers_should_expose_request_headers() - { - var feature = ServerTestContext.Request() - .Get("/test") - .Header("X-Custom", "val") - .BuildRequestFeature(); - var features = new TurboFeatureCollection(); - features.Set(feature); - var request = new TurboHttpRequest(features); - - Assert.Equal("val", request.Headers["X-Custom"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Query_should_parse_query_string() - { - var (request, _) = CreateRequest("GET", "/test?name=Alice&age=30"); - Assert.Equal("Alice", request.Query["name"].ToString()); - Assert.Equal("30", request.Query["age"].ToString()); - } - - [Fact(Timeout = 5000)] - public void ContentType_should_read_from_headers() - { - var feature = ServerTestContext.Request() - .Post("/test") - .Header("Content-Type", "application/json") - .BuildRequestFeature(); - var features = new TurboFeatureCollection(); - features.Set(feature); - var request = new TurboHttpRequest(features); - - Assert.Contains("application/json", request.ContentType); - } - - [Fact(Timeout = 5000)] - public void Host_should_parse_from_host_header() - { - var feature = ServerTestContext.Request() - .Get("/test") - .Host("example.com:8080") - .Header("Host", "example.com:8080") - .BuildRequestFeature(); - var features = new TurboFeatureCollection(); - features.Set(feature); - var request = new TurboHttpRequest(features); - - Assert.Equal("example.com:8080", request.Host.Value); - } - - private static (TurboHttpRequest Request, IFeatureCollection Features) CreateRequest( - string method, string path, string scheme = "http") - { - var feature = ServerTestContext.Request() - .Method(method) - .Path(path) - .Scheme(scheme) - .BuildRequestFeature(); - var features = new TurboFeatureCollection(); - features.Set(feature); - return (new TurboHttpRequest(features), features); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboHttpResponseSpec.cs b/src/TurboHTTP.Tests/Context/TurboHttpResponseSpec.cs deleted file mode 100644 index 512185c4f..000000000 --- a/src/TurboHTTP.Tests/Context/TurboHttpResponseSpec.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboHttpResponseSpec -{ - [Fact(Timeout = 5000)] - public void StatusCode_should_delegate_to_feature() - { - var (response, _) = CreateResponse(HttpStatusCode.NotFound); - Assert.Equal(404, response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void StatusCode_set_should_update_feature() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - response.StatusCode = 201; - Assert.Equal(201, response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void Headers_should_expose_response_headers() - { - var features = CreateFeatures(); - var feature = features.Get()!; - feature.Headers["X-Custom"] = "val"; - var response = new TurboHttpResponse(features); - - Assert.Equal("val", response.Headers["X-Custom"].ToString()); - } - - [Fact(Timeout = 5000)] - public void ContentType_should_set_header() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - response.ContentType = "text/plain"; - Assert.Equal("text/plain", response.ContentType); - } - - [Fact(Timeout = 5000)] - public void HasStarted_should_be_false_initially() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - Assert.False(response.HasStarted); - } - - [Fact(Timeout = 5000)] - public void Body_should_return_writable_stream() - { - var (response, features) = CreateResponse(HttpStatusCode.OK); - features.Set(new TurboHttpResponseBodyFeature()); - Assert.NotNull(response.Body); - } - - [Fact(Timeout = 5000)] - public void Redirect_should_set_status_and_location() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - response.Redirect("/new-location"); - Assert.Equal(302, response.StatusCode); - Assert.Equal("/new-location", response.Headers["Location"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Redirect_permanent_should_set_301() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - response.Redirect("/new-location", permanent: true); - Assert.Equal(301, response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void Redirect_should_accept_absolute_https_url() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - response.Redirect("https://example.com/path"); - Assert.Equal(302, response.StatusCode); - Assert.Equal("https://example.com/path", response.Headers["Location"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Redirect_should_reject_non_http_scheme() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - Assert.Throws(() => response.Redirect("javascript:alert(1)")); - } - - [Fact(Timeout = 5000)] - public void Redirect_should_reject_null_location() - { - var (response, _) = CreateResponse(HttpStatusCode.OK); - Assert.Throws(() => response.Redirect(null!)); - } - - private static (TurboHttpResponse Response, IFeatureCollection Features) CreateResponse(HttpStatusCode status) - { - var features = CreateFeatures((int)status); - return (new TurboHttpResponse(features), features); - } - - private static IFeatureCollection CreateFeatures(int statusCode = 200) - { - var feature = new TurboHttpResponseFeature - { - StatusCode = statusCode - }; - var features = new TurboFeatureCollection(); - features.Set(feature); - return features; - } -} - diff --git a/src/TurboHTTP.Tests/Context/TurboQueryCollectionSpec.cs b/src/TurboHTTP.Tests/Context/TurboQueryCollectionSpec.cs deleted file mode 100644 index 087fe3f6e..000000000 --- a/src/TurboHTTP.Tests/Context/TurboQueryCollectionSpec.cs +++ /dev/null @@ -1,57 +0,0 @@ -using TurboHTTP.Context.Adapters; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboQueryCollectionSpec -{ - [Fact(Timeout = 5000)] - public void Query_should_parse_single_parameter() - { - var query = new TurboQueryCollection("?name=Alice"); - Assert.Equal("Alice", query["name"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Query_should_parse_multiple_parameters() - { - var query = new TurboQueryCollection("?name=Alice&age=30"); - Assert.Equal("Alice", query["name"].ToString()); - Assert.Equal("30", query["age"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Query_should_return_empty_for_missing_key() - { - var query = new TurboQueryCollection("?name=Alice"); - Assert.Equal(Microsoft.Extensions.Primitives.StringValues.Empty, query["missing"]); - } - - [Fact(Timeout = 5000)] - public void Query_should_handle_empty_query_string() - { - var query = new TurboQueryCollection(""); - Assert.Equal(0, query.Count); - } - - [Fact(Timeout = 5000)] - public void Query_should_handle_null_query_string() - { - var query = new TurboQueryCollection(null); - Assert.Equal(0, query.Count); - } - - [Fact(Timeout = 5000)] - public void Query_should_decode_url_encoded_values() - { - var query = new TurboQueryCollection("?message=hello%20world"); - Assert.Equal("hello world", query["message"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Query_should_support_duplicate_keys() - { - var query = new TurboQueryCollection("?tag=a&tag=b"); - var values = query["tag"]; - Assert.Equal(2, values.Count); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboRequestBodyFeatureSpec.cs b/src/TurboHTTP.Tests/Context/TurboRequestBodyFeatureSpec.cs deleted file mode 100644 index 6b8ccb4bc..000000000 --- a/src/TurboHTTP.Tests/Context/TurboRequestBodyFeatureSpec.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Akka.Streams.Dsl; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboRequestBodyFeatureSpec -{ - [Fact(Timeout = 5000)] - public void TurboRequestBodyFeature_should_implement_iturorequestbodyfeature() - { - var feature = new TurboRequestBodyFeature(); - Assert.IsAssignableFrom(feature); - } - - [Fact(Timeout = 5000)] - public void TurboRequestBodyFeature_should_have_default_body_stream_null() - { - var feature = new TurboRequestBodyFeature(); - Assert.Equal(Stream.Null, feature.Body); - } - - [Fact(Timeout = 5000)] - public void TurboRequestBodyFeature_should_have_default_empty_body_source() - { - var feature = new TurboRequestBodyFeature(); - Assert.NotNull(feature.BodySource); - } - - [Fact(Timeout = 5000)] - public void TurboRequestBodyFeature_should_allow_setting_body() - { - var stream = new MemoryStream([1, 2, 3]); - var feature = new TurboRequestBodyFeature { Body = stream }; - Assert.Same(stream, feature.Body); - } - - [Fact(Timeout = 5000)] - public void TurboRequestBodyFeature_should_allow_setting_body_source() - { - var source = Source.Single>(new byte[] { 1, 2, 3 }); - var feature = new TurboRequestBodyFeature { BodySource = source }; - Assert.Same(source, feature.BodySource); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboRequestCookieCollectionSpec.cs b/src/TurboHTTP.Tests/Context/TurboRequestCookieCollectionSpec.cs deleted file mode 100644 index 704e3a816..000000000 --- a/src/TurboHTTP.Tests/Context/TurboRequestCookieCollectionSpec.cs +++ /dev/null @@ -1,50 +0,0 @@ -using TurboHTTP.Context.Adapters; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboRequestCookieCollectionSpec -{ - [Fact(Timeout = 5000)] - public void Cookie_should_parse_single_cookie() - { - var cookies = new TurboRequestCookieCollection("session=abc123"); - Assert.Equal("abc123", cookies["session"]); - } - - [Fact(Timeout = 5000)] - public void Cookie_should_parse_multiple_cookies() - { - var cookies = new TurboRequestCookieCollection("session=abc123; theme=dark"); - Assert.Equal("abc123", cookies["session"]); - Assert.Equal("dark", cookies["theme"]); - } - - [Fact(Timeout = 5000)] - public void Cookie_should_return_null_for_missing_key() - { - var cookies = new TurboRequestCookieCollection("session=abc123"); - Assert.Null(cookies["missing"]); - } - - [Fact(Timeout = 5000)] - public void Cookie_should_handle_empty_header() - { - var cookies = new TurboRequestCookieCollection(null); - Assert.Equal(0, cookies.Count); - } - - [Fact(Timeout = 5000)] - public void Cookie_should_enumerate_all_cookies() - { - var cookies = new TurboRequestCookieCollection("a=1; b=2; c=3"); - Assert.Equal(3, cookies.Count); - } - - [Fact(Timeout = 5000)] - public void Cookie_should_contain_key() - { - var cookies = new TurboRequestCookieCollection("session=abc123"); - Assert.True(cookies.ContainsKey("session")); - Assert.False(cookies.ContainsKey("missing")); - } -} diff --git a/src/TurboHTTP.Tests/Context/TurboResponseHeaderDictionarySpec.cs b/src/TurboHTTP.Tests/Context/TurboResponseHeaderDictionarySpec.cs deleted file mode 100644 index d9701b478..000000000 --- a/src/TurboHTTP.Tests/Context/TurboResponseHeaderDictionarySpec.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Microsoft.Extensions.Primitives; -using TurboHTTP.Context.Adapters; - -namespace TurboHTTP.Tests.Context; - -public sealed class TurboResponseHeaderDictionarySpec -{ - [Fact(Timeout = 5000)] - public void Indexer_should_return_stored_value() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-Custom"] = "value1" - }; - Assert.Equal("value1", dict["X-Custom"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Indexer_should_return_empty_for_missing_header() - { - var dict = new TurboResponseHeaderDictionary(); - Assert.Equal(StringValues.Empty, dict["X-Missing"]); - } - - [Fact(Timeout = 5000)] - public void Set_should_replace_header_value() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-Custom"] = "old", - ["X-Custom"] = "new" - }; - Assert.Equal("new", dict["X-Custom"].ToString()); - } - - [Fact(Timeout = 5000)] - public void ContentLength_should_read_from_content_length_header() - { - var dict = new TurboResponseHeaderDictionary - { - ["Content-Length"] = "100" - }; - Assert.Equal(100, dict.ContentLength); - } - - [Fact(Timeout = 5000)] - public void ContentLength_set_should_update_header() - { - var dict = new TurboResponseHeaderDictionary - { - ContentLength = 200 - }; - Assert.Equal("200", dict["Content-Length"].ToString()); - } - - [Fact(Timeout = 5000)] - public void Count_should_reflect_stored_headers() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-A"] = "1", - ["X-B"] = "2" - }; - Assert.Equal(2, dict.Count); - } - - [Fact(Timeout = 5000)] - public void Remove_should_delete_header() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-Custom"] = "value" - }; - Assert.True(dict.Remove("X-Custom")); - Assert.Equal(StringValues.Empty, dict["X-Custom"]); - } - - [Fact(Timeout = 5000)] - public void ContainsKey_should_find_existing_header() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-Custom"] = "value" - }; - Assert.True(dict.ContainsKey("X-Custom")); - Assert.False(dict.ContainsKey("X-Missing")); - } - - [Fact(Timeout = 5000)] - public void Clear_should_remove_all_headers() - { - var dict = new TurboResponseHeaderDictionary - { - ["X-A"] = "1", - ["X-B"] = "2" - }; - dict.Clear(); - Assert.Empty(dict); - } - - [Fact(Timeout = 5000)] - public void Keys_should_be_case_insensitive() - { - var dict = new TurboResponseHeaderDictionary - { - ["Content-Type"] = "text/html" - }; - Assert.Equal("text/html", dict["content-type"].ToString()); - } -} diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboServerInstrumentationSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboServerInstrumentationSpec.cs new file mode 100644 index 000000000..bb72512a0 --- /dev/null +++ b/src/TurboHTTP.Tests/Diagnostics/TurboServerInstrumentationSpec.cs @@ -0,0 +1,249 @@ +using System.Diagnostics; +using TurboHTTP.Diagnostics; +using static Servus.Core.Servus; + +namespace TurboHTTP.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class TurboServerInstrumentationSpec : IDisposable +{ + private readonly List _activities = []; + private readonly ActivityListener _listener; + + public TurboServerInstrumentationSpec() + { + var sourceName = Tracing.Source.Name; + _listener = new ActivityListener + { + ShouldListenTo = source => source.Name == sourceName, + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => _activities.Add(activity) + }; + ActivitySource.AddActivityListener(_listener); + } + + public void Dispose() + { + _listener.Dispose(); + foreach (var activity in _activities) + { + if (!activity.IsStopped) + { + activity.Stop(); + } + } + } + + [Fact(Timeout = 5000)] + public void IsServerTracingActive_should_return_true_when_listener_present() + { + Assert.True(Tracing.IsServerTracingActive()); + } + + [Fact(Timeout = 5000)] + public void StartConnectionActivity_should_create_server_activity() + { + var activity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp"); + + Assert.NotNull(activity); + Assert.Equal("TurboHTTP.Connection", activity.OperationName); + Assert.Equal(ActivityKind.Server, activity.Kind); + } + + [Fact(Timeout = 5000)] + public void StartConnectionActivity_should_set_server_tags() + { + var activity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + + Assert.Equal("127.0.0.1", activity.GetTagItem("server.address")); + Assert.Equal(8080, activity.GetTagItem("server.port")); + Assert.Equal("tcp", activity.GetTagItem("network.transport")); + } + + [Fact(Timeout = 5000)] + public void StopConnectionActivity_should_stop_activity() + { + var activity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + + Tracing.StopConnectionActivity(activity, null); + + Assert.True(activity.IsStopped); + } + + [Fact(Timeout = 5000)] + public void StopConnectionActivity_should_set_error_on_exception() + { + var activity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + + Tracing.StopConnectionActivity(activity, new IOException("Connection reset")); + + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal(typeof(IOException).FullName, activity.GetTagItem("error.type")); + } + + [Fact(Timeout = 5000)] + public void StartRequestActivity_should_create_child_of_connection() + { + var connActivity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + + var reqActivity = Tracing.StartRequestActivity("GET", "/api/data", "https")!; + + Assert.Equal("TurboHTTP.ServerRequest", reqActivity.OperationName); + Assert.Equal(ActivityKind.Server, reqActivity.Kind); + Assert.Equal(connActivity.TraceId, reqActivity.TraceId); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void StartRequestActivity_should_set_http_tags() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("POST", "/api/submit", "https")!; + + Assert.Equal("POST", reqActivity.GetTagItem("http.request.method")); + Assert.Equal("/api/submit", reqActivity.GetTagItem("url.path")); + Assert.Equal("https", reqActivity.GetTagItem("url.scheme")); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void SetServerResponse_should_set_status_code_tag() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("GET", "/", "http")!; + + Tracing.SetServerResponse(reqActivity, 200); + + Assert.Equal(200, reqActivity.GetTagItem("http.response.status_code")); + Assert.NotEqual(ActivityStatusCode.Error, reqActivity.Status); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void SetServerResponse_should_set_error_for_5xx() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("GET", "/", "http")!; + + Tracing.SetServerResponse(reqActivity, 500); + + Assert.Equal(500, reqActivity.GetTagItem("http.response.status_code")); + Assert.Equal("500", reqActivity.GetTagItem("error.type")); + Assert.Equal(ActivityStatusCode.Error, reqActivity.Status); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void SetServerResponse_should_set_error_for_4xx() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("GET", "/", "http")!; + + Tracing.SetServerResponse(reqActivity, 404); + + Assert.Equal(ActivityStatusCode.Error, reqActivity.Status); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void SetServerError_should_set_exception_details() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("GET", "/", "http")!; + + Tracing.SetServerError(reqActivity, new InvalidOperationException("Pipeline broken")); + + Assert.Equal(ActivityStatusCode.Error, reqActivity.Status); + Assert.Equal(typeof(InvalidOperationException).FullName, reqActivity.GetTagItem("error.type")); + Assert.Equal(typeof(InvalidOperationException).FullName, reqActivity.GetTagItem("exception.type")); + Assert.Equal("Pipeline broken", reqActivity.GetTagItem("exception.message")); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void AddBackpressureEvent_should_add_event_with_tags() + { + var connActivity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + + Tracing.AddBackpressureEvent(connActivity, 82, 100); + + var evt = Assert.Single(connActivity.Events, e => e.Name == "turbo.backpressure"); + Assert.Equal(82, evt.Tags.First(t => t.Key == "turbo.pipeline.inflight").Value); + Assert.Equal(100, evt.Tags.First(t => t.Key == "turbo.pipeline.max").Value); + } + + [Fact(Timeout = 5000)] + public void InjectConnectionTags_should_set_server_address_and_port() + { + var tags = new TagList(); + TurboServerInstrumentationExtensions.InjectConnectionTags(ref tags, "10.0.0.1", 443); + + Assert.Equal("10.0.0.1", tags.Single(t => t.Key == "server.address").Value); + Assert.Equal(443, tags.Single(t => t.Key == "server.port").Value); + } + + [Fact(Timeout = 5000)] + public void FullLifecycle_connection_with_request() + { + _activities.Clear(); + + var connActivity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("GET", "/health", "http")!; + + Tracing.SetServerResponse(reqActivity, 200); + reqActivity.Stop(); + + Tracing.StopConnectionActivity(connActivity, null); + + Assert.Equal(2, _activities.Count); + Assert.True(connActivity.IsStopped); + Assert.True(reqActivity.IsStopped); + Assert.Equal(connActivity.TraceId, reqActivity.TraceId); + } + + [Fact(Timeout = 5000)] + public void FullLifecycle_connection_with_error() + { + _activities.Clear(); + + var connActivity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("POST", "/api", "https")!; + + Tracing.SetServerError(reqActivity, new TimeoutException("Handler timed out")); + reqActivity.Stop(); + + Tracing.StopConnectionActivity(connActivity, null); + + Assert.Equal(2, _activities.Count); + Assert.Equal(ActivityStatusCode.Error, reqActivity.Status); + Assert.NotEqual(ActivityStatusCode.Error, connActivity.Status); + } + + [Fact(Timeout = 5000)] + public void StartRequestActivity_should_normalize_nonstandard_method() + { + _ = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp")!; + var reqActivity = Tracing.StartRequestActivity("PURGE", "/cache", "http")!; + + Assert.Equal("_OTHER", reqActivity.GetTagItem("http.request.method")); + Assert.Equal("PURGE", reqActivity.GetTagItem("http.request.method_original")); + + reqActivity.Stop(); + } + + [Fact(Timeout = 5000)] + public void StartConnectionActivity_should_return_null_when_no_listener() + { + _listener.Dispose(); + + var activity = Tracing.StartConnectionActivity("127.0.0.1", 8080, "tcp"); + + Assert.Null(activity); + } +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Diagnostics/TurboServerMetricsSpec.cs b/src/TurboHTTP.Tests/Diagnostics/TurboServerMetricsSpec.cs new file mode 100644 index 000000000..6ccdce082 --- /dev/null +++ b/src/TurboHTTP.Tests/Diagnostics/TurboServerMetricsSpec.cs @@ -0,0 +1,331 @@ +using System.Collections.Concurrent; +using System.Diagnostics.Metrics; +using TurboHTTP.Diagnostics; +using static Servus.Core.Servus; + +namespace TurboHTTP.Tests.Diagnostics; + +[Collection("OTEL")] +public sealed class TurboServerMetricsSpec : IDisposable +{ + private readonly MeterListener _listener; + private readonly ConcurrentBag> _longMeasurements = []; + private readonly ConcurrentBag> _doubleMeasurements = []; + + public TurboServerMetricsSpec() + { + var meterName = Metrics.Meter.Name; + _listener = new MeterListener(); + _listener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == meterName) + { + listener.EnableMeasurementEvents(instrument); + } + }; + + _listener.SetMeasurementEventCallback((instrument, measurement, tags, _) => + _longMeasurements.Add(new MetricMeasurement(instrument.Name, measurement, tags))); + + _listener.SetMeasurementEventCallback((instrument, measurement, tags, _) => + _doubleMeasurements.Add(new MetricMeasurement(instrument.Name, measurement, tags))); + + _listener.Start(); + } + + public void Dispose() + { + _listener.Dispose(); + } + + [Fact(Timeout = 5000)] + public void ActiveConnections_should_increment_and_decrement() + { + ClearMeasurements(); + + Metrics.ActiveConnections().Add(1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + Metrics.ActiveConnections().Add(-1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("kestrel.active_connections"); + Assert.Equal(2, measurements.Count); + Assert.Equal(0, measurements.Sum(m => m.Value)); + } + + [Fact(Timeout = 5000)] + public void ConnectionDuration_should_record_seconds() + { + ClearMeasurements(); + + Metrics.ConnectionDuration().Record(1.5, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080), + new KeyValuePair("network.protocol.version", "2")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("kestrel.connection.duration")); + Assert.Equal(1.5, m.Value); + } + + [Fact(Timeout = 5000)] + public void RejectedConnections_should_increment() + { + ClearMeasurements(); + + Metrics.RejectedConnections().Add(1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetLongMeasurements("kestrel.rejected_connections")); + Assert.Equal(1, m.Value); + } + + [Fact(Timeout = 5000)] + public void TlsHandshakeDuration_should_record() + { + ClearMeasurements(); + + Metrics.TlsHandshakeDuration().Record(0.05, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("tls.protocol.version", "1.3")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("kestrel.tls_handshake.duration")); + Assert.Equal(0.05, m.Value); + } + + [Fact(Timeout = 5000)] + public void ActiveTlsHandshakes_should_increment_and_decrement() + { + ClearMeasurements(); + + Metrics.ActiveTlsHandshakes().Add(1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 443)); + + Metrics.ActiveTlsHandshakes().Add(-1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 443)); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("kestrel.active_tls_handshakes"); + Assert.Equal(2, measurements.Count); + Assert.Equal(0, measurements.Sum(m => m.Value)); + } + + [Fact(Timeout = 5000)] + public void ServerActiveRequests_should_carry_method_and_scheme() + { + ClearMeasurements(); + + Metrics.ServerActiveRequests().Add(1, + new KeyValuePair("url.scheme", "https"), + new KeyValuePair("http.request.method", "GET")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetLongMeasurements("http.server.active_requests")); + Assert.Equal("https", GetTag(m.Tags, "url.scheme")); + Assert.Equal("GET", GetTag(m.Tags, "http.request.method")); + } + + [Fact(Timeout = 5000)] + public void ServerRequestDuration_should_record_with_status() + { + ClearMeasurements(); + + Metrics.ServerRequestDuration().Record(0.250, + new KeyValuePair("http.request.method", "POST"), + new KeyValuePair("http.response.status_code", 201), + new KeyValuePair("url.scheme", "https"), + new KeyValuePair("network.protocol.version", "2")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("http.server.request.duration")); + Assert.Equal(0.250, m.Value); + Assert.Equal("POST", GetTag(m.Tags, "http.request.method")); + Assert.Equal(201, GetTag(m.Tags, "http.response.status_code")); + } + + [Fact(Timeout = 5000)] + public void OTelStandard_instruments_should_have_correct_units() + { + Assert.Equal("{connection}", Metrics.ActiveConnections().Unit); + Assert.Equal("s", Metrics.ConnectionDuration().Unit); + Assert.Equal("{connection}", Metrics.RejectedConnections().Unit); + Assert.Equal("s", Metrics.TlsHandshakeDuration().Unit); + Assert.Equal("{handshake}", Metrics.ActiveTlsHandshakes().Unit); + Assert.Equal("{request}", Metrics.ServerActiveRequests().Unit); + Assert.Equal("s", Metrics.ServerRequestDuration().Unit); + } + + [Fact(Timeout = 5000)] + public void OTelStandard_instruments_should_have_descriptions() + { + Assert.False(string.IsNullOrEmpty(Metrics.ActiveConnections().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.ConnectionDuration().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.RejectedConnections().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.TlsHandshakeDuration().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.ActiveTlsHandshakes().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.ServerActiveRequests().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.ServerRequestDuration().Description)); + } + + [Fact(Timeout = 5000)] + public void PipelineInFlight_should_increment_and_decrement() + { + ClearMeasurements(); + + Metrics.PipelineInFlight().Add(1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + Metrics.PipelineInFlight().Add(-1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("turbo.server.pipeline.inflight"); + Assert.Equal(2, measurements.Count); + Assert.Equal(0, measurements.Sum(m => m.Value)); + } + + [Fact(Timeout = 5000)] + public void PipelinePending_should_track_reorder_buffer() + { + ClearMeasurements(); + + Metrics.PipelinePending().Add(1, + new KeyValuePair("server.address", "127.0.0.1"), + new KeyValuePair("server.port", 8080)); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetLongMeasurements("turbo.server.pipeline.pending")); + Assert.Equal(1, m.Value); + } + + [Fact(Timeout = 5000)] + public void HandlerTimeouts_should_increment() + { + ClearMeasurements(); + + Metrics.HandlerTimeouts().Add(1, + new KeyValuePair("server.address", "127.0.0.1")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetLongMeasurements("turbo.server.handler.timeouts")); + Assert.Equal(1, m.Value); + } + + [Fact(Timeout = 5000)] + public void DrainActive_should_track_draining_connections() + { + ClearMeasurements(); + + Metrics.DrainActive().Add(1); + Metrics.DrainActive().Add(-1); + + _listener.RecordObservableInstruments(); + + var measurements = GetLongMeasurements("turbo.server.drain.active"); + Assert.Equal(2, measurements.Count); + Assert.Equal(0, measurements.Sum(m => m.Value)); + } + + [Fact(Timeout = 5000)] + public void ProtocolNegotiationDuration_should_record() + { + ClearMeasurements(); + + Metrics.ProtocolNegotiationDuration().Record(0.002, + new KeyValuePair("network.protocol.version", "2")); + + _listener.RecordObservableInstruments(); + + var m = Assert.Single(GetDoubleMeasurements("turbo.server.protocol_negotiation.duration")); + Assert.Equal(0.002, m.Value); + Assert.Equal("2", GetTag(m.Tags, "network.protocol.version")); + } + + [Fact(Timeout = 5000)] + public void Differenzierung_instruments_should_have_correct_units() + { + Assert.Equal("{request}", Metrics.PipelineInFlight().Unit); + Assert.Equal("{request}", Metrics.PipelinePending().Unit); + Assert.Equal("{timeout}", Metrics.HandlerTimeouts().Unit); + Assert.Equal("{connection}", Metrics.DrainActive().Unit); + Assert.Equal("s", Metrics.ProtocolNegotiationDuration().Unit); + } + + [Fact(Timeout = 5000)] + public void Differenzierung_instruments_should_have_descriptions() + { + Assert.False(string.IsNullOrEmpty(Metrics.PipelineInFlight().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.PipelinePending().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.HandlerTimeouts().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.DrainActive().Description)); + Assert.False(string.IsNullOrEmpty(Metrics.ProtocolNegotiationDuration().Description)); + } + + private void ClearMeasurements() + { + _longMeasurements.Clear(); + _doubleMeasurements.Clear(); + } + + private List> GetLongMeasurements(string instrumentName) + { + return _longMeasurements + .Where(m => m.InstrumentName == instrumentName) + .ToList(); + } + + private List> GetDoubleMeasurements(string instrumentName) + { + return _doubleMeasurements + .Where(m => m.InstrumentName == instrumentName) + .ToList(); + } + + private static object? GetTag(ReadOnlySpan> tags, string key) + { + foreach (var tag in tags) + { + if (tag.Key == key) + { + return tag.Value; + } + } + + return null; + } + + private readonly record struct MetricMeasurement where T : struct + { + public string InstrumentName { get; } + public T Value { get; } + public KeyValuePair[] Tags { get; } + + public MetricMeasurement(string instrumentName, T value, ReadOnlySpan> tags) + { + InstrumentName = instrumentName; + Value = value; + Tags = tags.ToArray(); + } + } +} diff --git a/src/TurboHTTP.Tests/Features/Sse/SseFormatterFlowSpec.cs b/src/TurboHTTP.Tests/Features/Sse/SseFormatterFlowSpec.cs index ff2ae7890..28071802d 100644 --- a/src/TurboHTTP.Tests/Features/Sse/SseFormatterFlowSpec.cs +++ b/src/TurboHTTP.Tests/Features/Sse/SseFormatterFlowSpec.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.TestKit.Xunit; -using TurboHTTP.Features.Sse; +using Servus.Akka.Sse; namespace TurboHTTP.Tests.Features.Sse; diff --git a/src/TurboHTTP.Tests/Features/Sse/SseParserFlowSpec.cs b/src/TurboHTTP.Tests/Features/Sse/SseParserFlowSpec.cs index f6ff7f9fd..fa65647bd 100644 --- a/src/TurboHTTP.Tests/Features/Sse/SseParserFlowSpec.cs +++ b/src/TurboHTTP.Tests/Features/Sse/SseParserFlowSpec.cs @@ -3,7 +3,7 @@ using Akka.Streams; using Akka.Streams.Dsl; using Akka.TestKit.Xunit; -using TurboHTTP.Features.Sse; +using Servus.Akka.Sse; namespace TurboHTTP.Tests.Features.Sse; @@ -104,8 +104,8 @@ public async Task Flow_should_handle_split_across_chunks() { var result = await Source.From(new[] { - (ReadOnlyMemory)Encoding.UTF8.GetBytes("data: hel"), - (ReadOnlyMemory)Encoding.UTF8.GetBytes("lo\n\n") + (ReadOnlyMemory)"data: hel"u8.ToArray(), + (ReadOnlyMemory)"lo\n\n"u8.ToArray() }) .Via(SseParserFlow.Instance) .RunWith(Sink.Seq(), _materializer); @@ -118,7 +118,7 @@ public async Task Flow_should_handle_split_across_chunks() public async Task Flow_should_strip_bom() { var bom = new byte[] { 0xEF, 0xBB, 0xBF }; - var data = Encoding.UTF8.GetBytes("data: hello\n\n"); + var data = "data: hello\n\n"u8.ToArray(); var combined = bom.Concat(data).ToArray(); var result = await Source.Single((ReadOnlyMemory)combined) diff --git a/src/TurboHTTP.Tests/Protocol/ProtocolNegotiatingStateMachineSpec.cs b/src/TurboHTTP.Tests/Protocol/ProtocolNegotiatingStateMachineSpec.cs index 9bb2a6830..40758f01d 100644 --- a/src/TurboHTTP.Tests/Protocol/ProtocolNegotiatingStateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/ProtocolNegotiatingStateMachineSpec.cs @@ -104,7 +104,7 @@ public void DecodeClientData_should_select_http11_for_get_request() Assert.Single(ops.Requests); var ctx = ops.Requests[0]; - var feature = ctx.Features.Get(); + var feature = ctx.Get(); Assert.NotNull(feature); Assert.Equal("GET", feature.Method); } @@ -122,7 +122,7 @@ public void DecodeClientData_should_select_http11_for_post_request() Assert.Single(ops.Requests); var ctx = ops.Requests[0]; - var feature = ctx.Features.Get(); + var feature = ctx.Get(); Assert.NotNull(feature); Assert.Equal("POST", feature.Method); } diff --git a/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectChainSpec.cs b/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectChainSpec.cs index 6a164cd31..7e1636b24 100644 --- a/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectChainSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectChainSpec.cs @@ -312,4 +312,4 @@ public void RedirectChain_should_absorb_response_upstream_failure() requestOutProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken); responseOutProbe.ExpectComplete(TestContext.Current.CancellationToken); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectCoreSpec.cs b/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectCoreSpec.cs index 992f12612..d95799b5c 100644 --- a/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectCoreSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Semantics/Redirect/RedirectCoreSpec.cs @@ -330,4 +330,4 @@ public void RedirectCore_should_carry_redirect_handler_in_options() Assert.NotNull(handler); Assert.Equal(1, handler.RedirectCount); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryCoreSpec.cs b/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryCoreSpec.cs index c1928a778..9670c1a4c 100644 --- a/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryCoreSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryCoreSpec.cs @@ -382,4 +382,4 @@ public void RetryCore_should_retry_exactly_max_retries_minus_one_times_then_forw Assert.Same(finalResponse, respOut.ExpectNext(TestContext.Current.CancellationToken)); reqOut.ExpectNoMsg(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryTimerSpec.cs b/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryTimerSpec.cs index 09ee90601..60c908cab 100644 --- a/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryTimerSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Semantics/Retry/RetryTimerSpec.cs @@ -374,4 +374,4 @@ public void RetryTimer_should_complete_out_request_when_upstream_finished_and_no // OutRequest should complete (upstream done, no retries, in-flight resolved) requestOutProbe.ExpectComplete(TestContext.Current.CancellationToken); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSecuritySpec.cs index adaa2611d..ee63c6911 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSecuritySpec.cs @@ -151,7 +151,7 @@ public void Feed_should_not_crash_after_prior_error() var ex1 = Record.Exception(() => decoder.Feed(invalidCL, out _)); Assert.NotNull(ex1); - var ex2 = Record.Exception(() => decoder.Feed(validRequest, out _)); + _ = Record.Exception(() => decoder.Feed(validRequest, out _)); // Second feed may throw again, but should not crash with NullRef/AccessViolation // If it throws, it should be a protocol exception or similar, not a system-level crash } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSpec.cs index 8026fe206..4f46bcb0e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerDecoderSpec.cs @@ -65,7 +65,7 @@ public void Decoder_should_preserve_percent_encoded_request_uri() Assert.Equal(DecodeOutcome.Complete, decoder.Feed(raw, out _)); var feature = decoder.GetRequestFeature(); - Assert.Contains("%20", feature.RawTarget ?? ""); + Assert.Contains("%20", feature.RawTarget); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerEncoderFilteringSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerEncoderFilteringSpec.cs index b2b8ed687..6f17b41b3 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerEncoderFilteringSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerEncoderFilteringSpec.cs @@ -2,6 +2,7 @@ using TurboHTTP.Protocol.Syntax.Http10.Options; using TurboHTTP.Protocol.Syntax.Http10.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http10.Server; @@ -23,7 +24,7 @@ private static Http10ServerEncoder MakeEncoder(bool withDate = false) => public void EncodeDeferred_should_strip_hop_by_hop_header(string headerName) { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers[headerName] = "some-value"; + ctx.Get()?.Headers[headerName] = "some-value"; var buf = new byte[512]; var written = MakeEncoder(withDate: false).EncodeDeferred(buf, ctx, ReadOnlySpan.Empty); @@ -38,7 +39,7 @@ public void EncodeDeferred_should_strip_hop_by_hop_header(string headerName) public void EncodeDeferred_should_not_duplicate_existing_date_header() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["Date"] = DateTimeOffset.UtcNow.ToString("R"); + ctx.Get()?.Headers["Date"] = DateTimeOffset.UtcNow.ToString("R"); var buf = new byte[512]; var written = MakeEncoder(withDate: true).EncodeDeferred(buf, ctx, ReadOnlySpan.Empty); @@ -76,11 +77,15 @@ public void EncodeDeferred_should_emit_content_length_zero_for_empty_body() public void EncodeDeferred_should_strip_hop_by_hop_from_content_headers() { var ctx = ServerTestContext.CreateResponse(); - var hopByHopHeaders = new[] { "Connection", "Keep-Alive", "Transfer-Encoding", "TE", "Upgrade", "Proxy-Authenticate", "Proxy-Authorization", "Trailer" }; + var hopByHopHeaders = new[] + { + "Connection", "Keep-Alive", "Transfer-Encoding", "TE", "Upgrade", "Proxy-Authenticate", + "Proxy-Authorization", "Trailer" + }; foreach (var headerName in hopByHopHeaders) { - ctx.Response.Headers[headerName] = "some-value"; + ctx.Get()?.Headers[headerName] = "some-value"; } var buf = new byte[512]; @@ -123,4 +128,4 @@ public void EncodeDeferred_should_handle_status_with_empty_reason_phrase() Assert.StartsWith("HTTP/1.0 200", wireOutput); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineErrorSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineErrorSpec.cs index 65ad9ed98..61bd2a6a7 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineErrorSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineErrorSpec.cs @@ -3,10 +3,10 @@ using Akka.TestKit.Xunit; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http10.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http10.Server; @@ -15,21 +15,21 @@ public sealed class Http10ServerStateMachineErrorSpec : TestKit { private static FakeServerOps MakeOps() => new(); - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } - private static async Task CreateResponseContextWithBody(string body) + private static async Task CreateResponseContextWithBody(string body) { var context = CreateResponseContext(); - var bodyFeature = context.Features.Get()!; + var bodyFeature = context.Get()!; var bytes = Encoding.ASCII.GetBytes(body); await bodyFeature.Writer.WriteAsync(bytes); await bodyFeature.Writer.CompleteAsync(); @@ -129,5 +129,4 @@ public void OnBodyMessage_OutboundBodyFailed_should_not_crash_without_prior_resp Assert.Null(ex); } -} - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineSpec.cs index 77a656c2a..1992b0737 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http10/Server/Http10ServerStateMachineSpec.cs @@ -3,10 +3,10 @@ using Akka.TestKit.Xunit; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http10.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http10.Server; @@ -15,21 +15,21 @@ public sealed class Http10ServerStateMachineSpec : TestKit { private static FakeServerOps MakeOps() => new(); - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } - private static async Task CreateResponseContextWithBody(string body) + private static async Task CreateResponseContextWithBody(string body) { var context = CreateResponseContext(); - var bodyFeature = context.Features.Get()!; + var bodyFeature = context.Get()!; var bytes = Encoding.ASCII.GetBytes(body); await bodyFeature.Writer.WriteAsync(bytes); await bodyFeature.Writer.CompleteAsync(); @@ -57,13 +57,14 @@ public void DecodeClientData_should_decode_complete_request() sm.DecodeClientData(new TransportData(requestBuffer)); Assert.Single(ops.Requests); - Assert.Equal("GET", ops.Requests[0].Request.Method); - Assert.Equal("/path", ops.Requests[0].Request.Path.Value); + var req = ops.Requests[0].Get()!; + Assert.Equal("GET", req.Method); + Assert.Equal("/path", req.Path); } [Fact(Timeout = 5000)] [Trait("RFC", "RFC1945-5")] - public void DecodeClientData_should_mark_should_complete() + public void DecodeClientData_should_not_complete_before_response() { var ops = MakeOps(); var sm = new Http10ServerStateMachine(new TurboServerOptions(), ops); @@ -72,7 +73,8 @@ public void DecodeClientData_should_mark_should_complete() sm.DecodeClientData(new TransportData(requestBuffer)); - Assert.True(sm.ShouldComplete); + Assert.False(sm.ShouldComplete); + Assert.Single(ops.Requests); } [Fact(Timeout = 5000)] @@ -191,7 +193,8 @@ public void DecodeClientData_should_signal_error_for_unknown_method() sm.DecodeClientData(new TransportData(requestBuffer)); Assert.Single(ops.Requests); - Assert.Equal("PATCH", ops.Requests[0].Request.Method); + var req = ops.Requests[0].Get()!; + Assert.Equal("PATCH", req.Method); } [Fact(Timeout = 5000)] @@ -217,6 +220,11 @@ public void DecodeClientData_should_handle_post_without_content_length() var requestBuffer = CreateRequestBuffer("POST /path HTTP/1.0\r\nHost: example.com\r\n\r\n"); sm.DecodeClientData(new TransportData(requestBuffer)); - Assert.True(ops.Requests.Count == 0 || ops.Requests[0].Request.ContentLength is null or 0); + if (ops.Requests.Count > 0) + { + var req = ops.Requests[0].Get()!; + var contentLength = req.Headers["Content-Length"]; + Assert.True(string.IsNullOrEmpty(contentLength)); + } } } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Client/Http11HeaderReuseSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Client/Http11HeaderReuseSpec.cs new file mode 100644 index 000000000..d1ba7cbad --- /dev/null +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Client/Http11HeaderReuseSpec.cs @@ -0,0 +1,50 @@ +using System.Text; +using Akka.Actor; +using TurboHTTP.Protocol.Syntax.Http11.Client; +using TurboHTTP.Protocol.Syntax.Http11.Options; + +namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Client; + +public sealed class Http11HeaderReuseSpec +{ + [Fact(Timeout = 5000)] + public void Encode_should_produce_valid_output_on_second_call() + { + var encoder = new Http11ClientEncoder(Http11ClientEncoderOptions.Default); + + var request1 = new HttpRequestMessage(HttpMethod.Get, "http://example.com/first"); + var buffer1 = new byte[4 * 1024]; + var written1 = encoder.Encode(buffer1, request1, ActorRefs.Nobody); + var result1 = Encoding.ASCII.GetString(buffer1, 0, written1); + + var request2 = new HttpRequestMessage(HttpMethod.Post, "http://example.com/second"); + var buffer2 = new byte[4 * 1024]; + var written2 = encoder.Encode(buffer2, request2, ActorRefs.Nobody); + var result2 = Encoding.ASCII.GetString(buffer2, 0, written2); + + Assert.Contains("GET /first HTTP/1.1", result1); + Assert.Contains("POST /second HTTP/1.1", result2); + Assert.Contains("Host: example.com", result1); + Assert.Contains("Host: example.com", result2); + } + + [Fact(Timeout = 5000)] + public void Encode_should_not_leak_headers_between_calls() + { + var encoder = new Http11ClientEncoder(Http11ClientEncoderOptions.Default); + + var request1 = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); + request1.Headers.Add("X-Custom", "value1"); + var buffer1 = new byte[4 * 1024]; + var written1 = encoder.Encode(buffer1, request1, ActorRefs.Nobody); + var result1 = Encoding.ASCII.GetString(buffer1, 0, written1); + + var request2 = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); + var buffer2 = new byte[4 * 1024]; + var written2 = encoder.Encode(buffer2, request2, ActorRefs.Nobody); + var result2 = Encoding.ASCII.GetString(buffer2, 0, written2); + + Assert.Contains("X-Custom: value1", result1); + Assert.DoesNotContain("X-Custom", result2); + } +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/Http11FuzzBodySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/Http11FuzzBodySpec.cs index a7256675d..a9676d1f6 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/Http11FuzzBodySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/Http11FuzzBodySpec.cs @@ -38,7 +38,7 @@ private static void AssertDecodeEofNeverCrashes(Http11ClientDecoder decoder) if (decoder.SignalEof() || decoder.IsBodyComplete) { var response = decoder.GetResponse(); - response?.Dispose(); + response.Dispose(); decoder.Reset(); } } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/TlsSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/TlsSecuritySpec.cs index 3c9985f1b..5badaf556 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/TlsSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Security/TlsSecuritySpec.cs @@ -1,6 +1,7 @@ using TurboHTTP.Client; using System.Net; using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using Servus.Akka.Transport; using TurboHTTP.Internal; using TurboHTTP.Protocol.Semantics; @@ -74,16 +75,10 @@ public void TurboClientOptions_should_invoke_custom_callback_when_configured() { var invoked = false; SslPolicyErrors? observedErrors = null; - RemoteCertificateValidationCallback custom = (_, _, _, errors) => - { - invoked = true; - observedErrors = errors; - return errors is SslPolicyErrors.None; - }; var options = new TurboClientOptions { - ServerCertificateValidationCallback = custom, + ServerCertificateValidationCallback = Custom, }; var effective = options.EffectiveServerCertificateValidationCallback; @@ -93,23 +88,32 @@ public void TurboClientOptions_should_invoke_custom_callback_when_configured() Assert.True(invoked); Assert.Equal(SslPolicyErrors.RemoteCertificateChainErrors, observedErrors); + return; + + bool Custom(object o, X509Certificate? x509Certificate, X509Chain? x509Chain, SslPolicyErrors errors) + { + invoked = true; + observedErrors = errors; + return errors is SslPolicyErrors.None; + } } [Fact(Timeout = 5000)] public void TurboClientOptions_should_respect_custom_callback_decision_when_accepting() { - // Scenario: Custom callback implements pinning or custom CA trust. - RemoteCertificateValidationCallback alwaysAccept = (_, _, _, _) => true; - var options = new TurboClientOptions { - ServerCertificateValidationCallback = alwaysAccept, + ServerCertificateValidationCallback = AlwaysAccept, }; var effective = options.EffectiveServerCertificateValidationCallback!; // Should accept even chain errors via custom policy Assert.True(effective(null!, null, null, SslPolicyErrors.RemoteCertificateChainErrors)); + return; + + // Scenario: Custom callback implements pinning or custom CA trust. + bool AlwaysAccept(object o, X509Certificate? x509Certificate, X509Chain? x509Chain, SslPolicyErrors sslPolicyErrors) => true; } [Fact(Timeout = 5000)] @@ -138,15 +142,10 @@ public void TurboClientOptions_should_bypass_custom_callback_when_dangerous_acce public void TcpOptionsFactory_should_propagate_custom_callback_when_building_tls_options() { var invoked = false; - RemoteCertificateValidationCallback custom = (_, _, _, _) => - { - invoked = true; - return true; - }; var options = new TurboClientOptions { - ServerCertificateValidationCallback = custom, + ServerCertificateValidationCallback = Custom, }; var uri = new Uri("https://example.com/"); @@ -162,6 +161,13 @@ public void TcpOptionsFactory_should_propagate_custom_callback_when_building_tls Assert.NotNull(tlsOptions.ServerCertificateValidationCallback); tlsOptions.ServerCertificateValidationCallback!(null!, null, null, SslPolicyErrors.None); Assert.True(invoked); + return; + + bool Custom(object o, X509Certificate? x509Certificate, X509Chain? x509Chain, SslPolicyErrors sslPolicyErrors) + { + invoked = true; + return true; + } } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerBodyDrainingSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerBodyDrainingSpec.cs index 25282f7d4..e913a6bcb 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerBodyDrainingSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerBodyDrainingSpec.cs @@ -13,7 +13,7 @@ public void ContentLengthBufferedDecoder_IsComplete_should_return_true_when_all_ { var decoder = new ContentLengthBufferedDecoder(10, MemoryPool.Shared); - var data = Encoding.ASCII.GetBytes("0123456789"); + var data = "0123456789"u8.ToArray(); decoder.Feed(data, out _); Assert.True(decoder.IsComplete); @@ -24,7 +24,7 @@ public void ContentLengthBufferedDecoder_IsComplete_should_return_false_when_inc { var decoder = new ContentLengthBufferedDecoder(10, MemoryPool.Shared); - var data = Encoding.ASCII.GetBytes("01234"); + var data = "01234"u8.ToArray(); decoder.Feed(data, out _); Assert.False(decoder.IsComplete); @@ -35,11 +35,11 @@ public void ContentLengthBufferedDecoder_Drain_should_skip_remaining_bytes() { var decoder = new ContentLengthBufferedDecoder(10, MemoryPool.Shared); - var data = Encoding.ASCII.GetBytes("012"); + var data = "012"u8.ToArray(); decoder.Feed(data, out _); Assert.False(decoder.IsComplete); - var remaining = Encoding.ASCII.GetBytes("3456789"); + var remaining = "3456789"u8.ToArray(); var drained = decoder.Drain(remaining); Assert.Equal(7, drained); @@ -51,11 +51,11 @@ public void ContentLengthBufferedDecoder_Drain_should_return_zero_when_complete( { var decoder = new ContentLengthBufferedDecoder(5, MemoryPool.Shared); - var data = Encoding.ASCII.GetBytes("01234"); + var data = "01234"u8.ToArray(); decoder.Feed(data, out _); Assert.True(decoder.IsComplete); - var drained = decoder.Drain(Encoding.ASCII.GetBytes("extra")); + var drained = decoder.Drain("extra"u8); Assert.Equal(0, drained); } @@ -65,10 +65,10 @@ public void ContentLengthBufferedDecoder_Drain_should_consume_only_needed_bytes( { var decoder = new ContentLengthBufferedDecoder(10, MemoryPool.Shared); - var data = Encoding.ASCII.GetBytes("01234"); + var data = "01234"u8.ToArray(); decoder.Feed(data, out _); - var remaining = Encoding.ASCII.GetBytes("567890extra"); + var remaining = "567890extra"u8.ToArray(); var drained = decoder.Drain(remaining); Assert.Equal(5, drained); @@ -80,7 +80,7 @@ public void ContentLengthStreamedDecoder_IsComplete_should_return_true_when_all_ { var decoder = new ContentLengthStreamedDecoder(10); - var data = Encoding.ASCII.GetBytes("0123456789"); + var data = "0123456789"u8.ToArray(); decoder.Feed(data, out _); Assert.True(decoder.IsComplete); @@ -91,7 +91,7 @@ public void ContentLengthStreamedDecoder_IsComplete_should_return_false_when_inc { var decoder = new ContentLengthStreamedDecoder(10); - var data = Encoding.ASCII.GetBytes("01234"); + var data = "01234"u8.ToArray(); decoder.Feed(data, out _); Assert.False(decoder.IsComplete); @@ -102,11 +102,11 @@ public void ContentLengthStreamedDecoder_Drain_should_skip_remaining_bytes() { var decoder = new ContentLengthStreamedDecoder(10); - var data = Encoding.ASCII.GetBytes("012"); + var data = "012"u8.ToArray(); decoder.Feed(data, out _); Assert.False(decoder.IsComplete); - var remaining = Encoding.ASCII.GetBytes("3456789"); + var remaining = "3456789"u8.ToArray(); var drained = decoder.Drain(remaining); Assert.Equal(7, drained); @@ -118,11 +118,11 @@ public void ContentLengthStreamedDecoder_Drain_should_return_zero_when_complete( { var decoder = new ContentLengthStreamedDecoder(5); - var data = Encoding.ASCII.GetBytes("01234"); + var data = "01234"u8.ToArray(); decoder.Feed(data, out _); Assert.True(decoder.IsComplete); - var drained = decoder.Drain(Encoding.ASCII.GetBytes("extra")); + var drained = decoder.Drain("extra"u8); Assert.Equal(0, drained); } @@ -206,4 +206,4 @@ public void Http11ServerStateMachine_should_expose_null_body_decoder_when_reset( Assert.Null(decoder.CurrentBodyDecoder); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerConnectionPersistenceSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerConnectionPersistenceSpec.cs index ced50a4e6..000e1905e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerConnectionPersistenceSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerConnectionPersistenceSpec.cs @@ -1,24 +1,24 @@ using System.Text; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; public sealed class Http11ServerConnectionPersistenceSpec { - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSecuritySpec.cs index 699520fc1..3bdcf51f4 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSecuritySpec.cs @@ -20,13 +20,13 @@ private static Http11ServerDecoder MakeDecoder(SharedHttpOptions? shared = null) public void Feed_should_reject_request_with_both_content_length_and_transfer_encoding() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 5\r\n" + - "Transfer-Encoding: chunked\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 5\r\n" + + "Transfer-Encoding: chunked\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); - _ = Assert.Throws(() => decoder.Feed(bytes, out _)); + Assert.Throws(() => decoder.Feed(bytes, out _)); } [Fact(Timeout = 5000)] @@ -34,12 +34,12 @@ public void Feed_should_reject_request_with_both_content_length_and_transfer_enc public void Feed_should_reject_transfer_encoding_in_http10_request() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.0\r\n" + - "Host: example.com\r\n" + - "Transfer-Encoding: chunked\r\n\r\n"; + const string request = "POST / HTTP/1.0\r\n" + + "Host: example.com\r\n" + + "Transfer-Encoding: chunked\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); - _ = Assert.Throws(() => decoder.Feed(bytes, out _)); + Assert.Throws(() => decoder.Feed(bytes, out _)); } [Fact(Timeout = 5000)] @@ -47,13 +47,13 @@ public void Feed_should_reject_transfer_encoding_in_http10_request() public void Feed_should_reject_conflicting_content_length_values() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 5\r\n" + - "Content-Length: 10\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 5\r\n" + + "Content-Length: 10\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); - _ = Assert.Throws(() => decoder.Feed(bytes, out _)); + Assert.Throws(() => decoder.Feed(bytes, out _)); } [Fact(Timeout = 5000)] @@ -61,12 +61,12 @@ public void Feed_should_reject_conflicting_content_length_values() public void Feed_should_reject_non_numeric_content_length() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: abc\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: abc\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); - _ = Assert.Throws(() => decoder.Feed(bytes, out _)); + Assert.Throws(() => decoder.Feed(bytes, out _)); } [Fact(Timeout = 5000)] @@ -74,12 +74,12 @@ public void Feed_should_reject_non_numeric_content_length() public void Feed_should_reject_negative_content_length() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: -1\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: -1\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); - _ = Assert.Throws(() => decoder.Feed(bytes, out _)); + Assert.Throws(() => decoder.Feed(bytes, out _)); } [Fact(Timeout = 5000)] @@ -87,11 +87,11 @@ public void Feed_should_reject_negative_content_length() public void Feed_should_accept_duplicate_content_length_with_same_value() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 5\r\n" + - "Content-Length: 5\r\n\r\n" + - "hello"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 5\r\n" + + "Content-Length: 5\r\n\r\n" + + "hello"; var bytes = Encoding.ASCII.GetBytes(request); var outcome = decoder.Feed(bytes, out _); @@ -106,12 +106,12 @@ public void Feed_should_accept_duplicate_content_length_with_same_value() public void Feed_should_parse_chunked_request_body() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Transfer-Encoding: chunked\r\n\r\n" + - "5\r\n" + - "hello\r\n" + - "0\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Transfer-Encoding: chunked\r\n\r\n" + + "5\r\n" + + "hello\r\n" + + "0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); var outcome = decoder.Feed(bytes, out _); @@ -124,12 +124,12 @@ public void Feed_should_parse_chunked_request_body() public void Feed_should_accept_chunk_size_with_leading_zeros() { var decoder = MakeDecoder(); - var request = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Transfer-Encoding: chunked\r\n\r\n" + - "0005\r\n" + - "hello\r\n" + - "0\r\n\r\n"; + const string request = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Transfer-Encoding: chunked\r\n\r\n" + + "0005\r\n" + + "hello\r\n" + + "0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); var outcome = decoder.Feed(bytes, out _); @@ -142,10 +142,10 @@ public void Feed_should_accept_chunk_size_with_leading_zeros() public void HasConnectionClose_should_detect_close_case_insensitive() { var decoder = MakeDecoder(); - var request = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Connection: CLOSE\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: CLOSE\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); _ = decoder.Feed(bytes, out _); @@ -158,10 +158,10 @@ public void HasConnectionClose_should_detect_close_case_insensitive() public void HasConnectionClose_should_be_false_for_keep_alive() { var decoder = MakeDecoder(); - var request = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Connection: keep-alive\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); _ = decoder.Feed(bytes, out _); @@ -173,9 +173,9 @@ public void HasConnectionClose_should_be_false_for_keep_alive() public void Reset_should_be_idempotent() { var decoder = MakeDecoder(); - var request = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); _ = decoder.Feed(bytes, out _); @@ -190,9 +190,9 @@ public void Reset_should_be_idempotent() public void Reset_should_allow_decoding_next_request() { var decoder = MakeDecoder(); - var request1 = "GET /first HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request1 = "GET /first HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes1 = Encoding.ASCII.GetBytes(request1); _ = decoder.Feed(bytes1, out _); @@ -200,9 +200,9 @@ public void Reset_should_allow_decoding_next_request() decoder.Reset(); - var request2 = "POST /second HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request2 = "POST /second HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes2 = Encoding.ASCII.GetBytes(request2); _ = decoder.Feed(bytes2, out _); var feature2 = decoder.GetRequestFeature(); @@ -219,11 +219,11 @@ public void Feed_should_reject_obs_fold_when_not_allowed() { var shared = new SharedHttpOptions { AllowObsFold = false }; var decoder = MakeDecoder(shared); - var request = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "X-Custom: value\r\n" + - " continued\r\n" + - "Content-Length: 0\r\n\r\n"; + const string request = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "X-Custom: value\r\n" + + " continued\r\n" + + "Content-Length: 0\r\n\r\n"; var bytes = Encoding.ASCII.GetBytes(request); _ = Assert.Throws(() => decoder.Feed(bytes, out _)); @@ -233,9 +233,9 @@ public void Feed_should_reject_obs_fold_when_not_allowed() public void Feed_should_not_crash_after_prior_error() { var decoder = MakeDecoder(); - var badRequest = "POST / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: abc\r\n\r\n"; + const string badRequest = "POST / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: abc\r\n\r\n"; var badBytes = Encoding.ASCII.GetBytes(badRequest); // Feed invalid request @@ -243,9 +243,9 @@ public void Feed_should_not_crash_after_prior_error() // Reset and feed valid request decoder.Reset(); - var validRequest = "GET / HTTP/1.1\r\n" + - "Host: example.com\r\n" + - "Content-Length: 0\r\n\r\n"; + const string validRequest = "GET / HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "Content-Length: 0\r\n\r\n"; var validBytes = Encoding.ASCII.GetBytes(validRequest); var exception = Record.Exception(() => decoder.Feed(validBytes, out _)); @@ -253,4 +253,4 @@ public void Feed_should_not_crash_after_prior_error() // Should not throw NullReferenceException or other crashes Assert.Null(exception); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSpec.cs index c5575e80b..fc08c62b3 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerDecoderSpec.cs @@ -124,7 +124,7 @@ public void Feed_should_accept_absolute_form_request_target() Assert.Equal(DecodeOutcome.Complete, outcome); var feature = decoder.GetRequestFeature(); Assert.Equal("GET", feature.Method); - Assert.Contains("/path", feature.Path ?? ""); + Assert.Contains("/path", feature.Path); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderHardeningSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderHardeningSpec.cs index b4c21b157..e6b4e4480 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderHardeningSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderHardeningSpec.cs @@ -2,6 +2,7 @@ using TurboHTTP.Protocol.Syntax.Http11.Options; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; @@ -24,7 +25,7 @@ public void Encode_should_strip_hop_by_hop_header(string headerName) { var encoder = MakeEncoder(); var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers[headerName] = "test-value"; + ctx.Get()?.Headers[headerName] = "test-value"; var buffer = new byte[4096]; var written = encoder.Encode(buffer, ctx, isChunked: false); @@ -66,9 +67,9 @@ public void Encode_should_not_add_content_length_when_chunked() public void Encode_should_not_duplicate_existing_date_header() { var encoder = MakeEncoder(withDate: true); - var existingDate = "Mon, 17 May 2021 12:00:00 GMT"; + const string existingDate = "Mon, 17 May 2021 12:00:00 GMT"; var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["Date"] = existingDate; + ctx.Get()?.Headers["Date"] = existingDate; var buffer = new byte[4096]; var written = encoder.Encode(buffer, ctx, isChunked: false); @@ -77,4 +78,4 @@ public void Encode_should_not_duplicate_existing_date_header() var dateCount = result.Split("Date:").Length - 1; Assert.Equal(1, dateCount); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderSpec.cs index ea115be3e..fa896924e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerEncoderSpec.cs @@ -2,6 +2,7 @@ using TurboHTTP.Protocol.Syntax.Http11.Options; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; @@ -26,7 +27,7 @@ public void Encode_should_write_status_line() public void Encode_should_add_content_length() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["Content-Length"] = "9"; + ctx.Get()?.Headers["Content-Length"] = "9"; var buffer = new byte[4096]; var written = _encoder.Encode(buffer, ctx, isChunked: false); @@ -65,7 +66,7 @@ public void Encode_should_include_date_header() public void Encode_should_not_produce_bare_cr_in_headers() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["X-Test"] = "value\rwith\rcr"; + ctx.Get()?.Headers["X-Test"] = "value\rwith\rcr"; var buffer = new byte[4096]; var written = _encoder.Encode(buffer, ctx, isChunked: false); @@ -85,7 +86,7 @@ public void Encode_should_not_produce_bare_cr_in_headers() public void Encode_should_not_produce_obs_fold_in_headers() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["X-Long"] = new string('a', 200); + ctx.Get()?.Headers["X-Long"] = new string('a', 200); var buffer = new byte[4096]; var written = _encoder.Encode(buffer, ctx, isChunked: false); @@ -114,7 +115,7 @@ public void Encode_should_not_double_apply_chunked_transfer_encoding() public void Encode_should_include_content_length_for_known_size_body() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["Content-Length"] = "15"; + ctx.Get()?.Headers["Content-Length"] = "15"; var buffer = new byte[4096]; var written = _encoder.Encode(buffer, ctx, isChunked: false); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningLimitSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningLimitSpec.cs index f468b7680..823014732 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningLimitSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningLimitSpec.cs @@ -1,24 +1,24 @@ using System.Text; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; public sealed class Http11ServerPipeliningLimitSpec { - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningSpec.cs index 2d1fab5c0..c30bb32bc 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerPipeliningSpec.cs @@ -1,24 +1,24 @@ using System.Text; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; public sealed class Http11ServerPipeliningSpec { - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } [Fact(Timeout = 5000)] @@ -41,8 +41,8 @@ public void ServerStateMachine_should_decode_two_pipelined_requests_from_single_ sm.DecodeClientData(new TransportData(buffer)); Assert.Equal(2, ops.Requests.Count); - Assert.Equal("/", ops.Requests[0].Request.Path.Value); - Assert.Equal("/page2", ops.Requests[1].Request.Path.Value); + Assert.Equal("/", ops.Requests[0].Get()?.Path); + Assert.Equal("/page2", ops.Requests[1].Get()?.Path); } [Fact(Timeout = 5000)] @@ -109,9 +109,9 @@ public void ServerStateMachine_should_handle_three_pipelined_requests() sm.DecodeClientData(new TransportData(buffer)); Assert.Equal(3, ops.Requests.Count); - Assert.Equal("/page1", ops.Requests[0].Request.Path.Value); - Assert.Equal("/page2", ops.Requests[1].Request.Path.Value); - Assert.Equal("/page3", ops.Requests[2].Request.Path.Value); + Assert.Equal("/page1", ops.Requests[0].Get()?.Path); + Assert.Equal("/page2", ops.Requests[1].Get()?.Path); + Assert.Equal("/page3", ops.Requests[2].Get()?.Path); } private static TransportBuffer MakeBuffer(string raw) diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineConnectionSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineConnectionSpec.cs index c5dadd1b6..8ea1a8d50 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineConnectionSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineConnectionSpec.cs @@ -2,25 +2,25 @@ using System.Text; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; public sealed class Http11ServerStateMachineConnectionSpec { - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } private static TransportBuffer MakeBuffer(string raw) @@ -39,7 +39,7 @@ public void ShouldComplete_should_be_true_when_connection_close_on_request() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -55,7 +55,7 @@ public void ShouldComplete_should_be_true_for_http10_request_on_h11_connection() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -71,7 +71,7 @@ public void OnResponse_should_include_connection_close_when_ShouldComplete() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -93,7 +93,7 @@ public void DecodeClientData_should_set_ShouldComplete_on_decode_error() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var invalidRequest = "INVALID REQUEST DATA\r\n\r\n"; + const string invalidRequest = "INVALID REQUEST DATA\r\n\r\n"; var buffer = MakeBuffer(invalidRequest); sm.DecodeClientData(new TransportData(buffer)); @@ -108,7 +108,7 @@ public void OnBodyMessage_OutboundBodyFailed_should_clear_pending_flag() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -137,7 +137,7 @@ public void OnBodyMessage_multi_chunk_should_emit_all_chunks() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -176,7 +176,7 @@ public void Cleanup_should_be_idempotent() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); @@ -212,13 +212,13 @@ public void OnResponse_should_set_chunked_transfer_encoding_when_no_content_leng var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); var context = CreateResponseContext(); - context.Response.StatusCode = 200; - context.Response.ContentType = "text/event-stream"; + context.Get()?.StatusCode = 200; + context.Get()?.Headers["Content-Type"] = "text/event-stream"; sm.OnResponse(context); @@ -237,13 +237,13 @@ public void OnResponse_should_not_set_chunked_when_content_length_present() var ops = new FakeServerOps(); var sm = new Http11ServerStateMachine(new TurboServerOptions(), ops); - var requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; + const string requestData = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n"; var buffer = MakeBuffer(requestData); sm.DecodeClientData(new TransportData(buffer)); var context = CreateResponseContext(); - context.Response.StatusCode = 200; - context.Response.Headers["Content-Length"] = "5"; + context.Get()?.StatusCode = 200; + context.Get()?.Headers["Content-Length"] = "5"; sm.OnResponse(context); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineTimerSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineTimerSpec.cs index b9da5b06c..7f0494b7a 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineTimerSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11ServerStateMachineTimerSpec.cs @@ -1,25 +1,25 @@ using System.Text; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; public sealed class Http11ServerStateMachineTimerSpec { - private static TurboHttpContext CreateResponseContext() + private static IFeatureCollection CreateResponseContext() { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } private static TransportBuffer MakeBuffer(string raw) @@ -174,7 +174,4 @@ public void Cleanup_should_cancel_all_timers() Assert.Contains(ops.CancelledTimers, t => t == "request-headers"); Assert.Contains(ops.CancelledTimers, t => t == "keep-alive"); } -} - - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11UpgradeH2cSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11UpgradeH2cSpec.cs index ed2d20236..f3b473881 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11UpgradeH2cSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/Http11UpgradeH2cSpec.cs @@ -2,6 +2,7 @@ using Akka.Actor; using Akka.Event; using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http11.Server; @@ -11,22 +12,32 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; -public sealed class Http11UpgradeH2cSpec +public sealed class Http11UpgradeH2CSpec { private sealed class SwitchCapableOps : IServerStageOperations, IProtocolSwitchCapable { private readonly FakeServerOps _inner = new(); public Func? SwitchFactory { get; private set; } - public List Requests => _inner.Requests; + public List Requests => _inner.Requests; public List Outbound => _inner.Outbound; public List<(string Name, TimeSpan Delay)> ScheduledTimers => _inner.ScheduledTimers; public List CancelledTimers => _inner.CancelledTimers; public ILoggingAdapter Log => _inner.Log; - public IActorRef StageActor { get => _inner.StageActor; set => _inner.StageActor = value; } - public IMaterializer Materializer { get => _inner.Materializer; set => _inner.Materializer = value; } - public void OnRequest(TurboHttpContext context) => _inner.OnRequest(context); + public IActorRef StageActor + { + get => _inner.StageActor; + set => _inner.StageActor = value; + } + + public IMaterializer Materializer + { + get => _inner.Materializer; + set => _inner.Materializer = value; + } + + public void OnRequest(IFeatureCollection features) => _inner.OnRequest(features); public void OnOutbound(ITransportOutbound item) => _inner.OnOutbound(item); public void OnScheduleTimer(string name, TimeSpan delay) => _inner.OnScheduleTimer(name, delay); public void OnCancelTimer(string name) => _inner.OnCancelTimer(name); @@ -87,7 +98,7 @@ public void DecodeClientData_should_ignore_upgrade_when_ops_not_switchable() "\r\n")); Assert.Single(ops.Requests); - Assert.Equal("GET", ops.Requests[0].Request.Method); + Assert.Equal("GET", ops.Requests[0].Get()?.Method); } [Fact(Timeout = 5000)] @@ -108,6 +119,4 @@ public void DecodeClientData_should_ignore_upgrade_without_http2_settings() Assert.Null(ops.SwitchFactory); Assert.Single(ops.Requests); } -} - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/ServerStateMachineSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/ServerStateMachineSpec.cs index c8a24b071..4736e4c80 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/ServerStateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http11/Server/ServerStateMachineSpec.cs @@ -2,10 +2,10 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http11.Server; @@ -33,8 +33,8 @@ public void DecodeClientData_should_emit_request_when_complete_get() Assert.Single(ops.Requests); var ctx = ops.Requests[0]; - Assert.Equal("GET", ctx.Request.Method); - Assert.Equal("/", ctx.Request.Path.Value); + Assert.Equal("GET", ctx.Get()?.Method); + Assert.Equal("/", ctx.Get()?.Path); } [Fact(Timeout = 5000)] @@ -366,10 +366,10 @@ public void DecodeClientData_should_pass_unknown_transfer_encoding_to_applicatio // which is responsible for inspecting TE and returning 501. The SM correctly decodes // the request structure and preserves the TE header for application inspection. Assert.Single(ops.Requests); - Assert.Equal("POST", ops.Requests[0].Request.Method); + Assert.Equal("POST", ops.Requests[0].Get()?.Method); } - private static TurboHttpContext MakeResponseContext(HttpResponseMessage response) + private static IFeatureCollection MakeResponseContext(HttpResponseMessage response) { var features = new TurboFeatureCollection(); var responseFeature = new TurboHttpResponseFeature @@ -395,10 +395,10 @@ private static TurboHttpContext MakeResponseContext(HttpResponseMessage response { var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); + features.Set(bodyFeature); } features.Set(responseFeature); - return new TurboHttpContext(features); + return features; } } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/Decoder/ConnectTunnelSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/Decoder/ConnectTunnelSpec.cs index ee2d354c6..edc1f82c5 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/Decoder/ConnectTunnelSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/Decoder/ConnectTunnelSpec.cs @@ -20,8 +20,8 @@ public void Encoder_should_omit_scheme_and_path_when_connect_method() Assert.DoesNotContain(headers, h => h.Name == ":scheme"); Assert.DoesNotContain(headers, h => h.Name == ":path"); - Assert.Contains(headers, h => h.Name == ":method" && h.Value == "CONNECT"); - Assert.Contains(headers, h => h.Name == ":authority" && h.Value == "proxy.example.com:443"); + Assert.Contains(headers, h => h is { Name: ":method", Value: "CONNECT" }); + Assert.Contains(headers, h => h is { Name: ":authority", Value: "proxy.example.com:443" }); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/StateMachine/Http2RstStreamRestrictionSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/StateMachine/Http2RstStreamRestrictionSpec.cs index 1422a9194..bf32b71f2 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/StateMachine/Http2RstStreamRestrictionSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Client/StateMachine/Http2RstStreamRestrictionSpec.cs @@ -10,9 +10,6 @@ private static byte[] MakeRstStreamBytes(int streamId, Http2ErrorCode errorCode) private static byte[] MakeWindowUpdateBytes(int streamId, int increment) => new WindowUpdateFrame(streamId, increment).Serialize(); - private static byte[] MakeDataBytes(int streamId, bool endStream) - => new DataFrame(streamId, "data"u8.ToArray(), endStream).Serialize(); - private static byte[] Concat(params byte[][] arrays) { var result = new byte[arrays.Sum(a => a.Length)]; diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Security/Http2SecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Security/Http2SecuritySpec.cs index 7cd762d61..8e17f67da 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Security/Http2SecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Security/Http2SecuritySpec.cs @@ -127,8 +127,7 @@ public void Http2FrameDecoder_should_detect_rst_flood_when_explicit_enforcement_ decoder.Decode(rst101); // Decoder still accepts it rstCount++; // Count reaches 101 - var ex = Assert.Throws(() => - EnforceRstFloodThreshold(rstCount, threshold: 100)); + Assert.Throws(() => EnforceRstFloodThreshold(rstCount, threshold: 100)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Http2ServerDecoderSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Http2ServerDecoderSecuritySpec.cs index 765f20515..dc51d6bb9 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Http2ServerDecoderSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Http2ServerDecoderSecuritySpec.cs @@ -9,8 +9,6 @@ public sealed class Http2ServerDecoderSecuritySpec private readonly HpackEncoder _encoder = new(useHuffman: false); private readonly Http2ServerDecoder _decoder = new(); - #region Pseudo-Header Validation (RFC 9113 §8.3) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-8.3")] public void DecodeHeaders_should_reject_duplicate_method_pseudo_header() @@ -102,10 +100,6 @@ public void DecodeHeaders_should_reject_unknown_pseudo_header() Assert.Contains(":custom", ex.Message); } - #endregion - - #region Forbidden Connection Headers (RFC 9113 §8.2.2) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-8.2.2")] public void DecodeHeaders_should_reject_connection_header() @@ -197,10 +191,6 @@ public void DecodeHeaders_should_accept_te_header_with_trailers_value() Assert.Equal("GET", feature.Method); } - #endregion - - #region CONNECT Edge Cases (RFC 9113 §8.5) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-8.5")] public void DecodeHeaders_CONNECT_with_path_should_reject() @@ -262,10 +252,6 @@ public void DecodeHeaders_CONNECT_without_authority_should_reject() Assert.Contains(":authority", ex.Message); } - #endregion - - #region Header Size Limits (RFC 9113 §10.5.1) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-10.5.1")] public void DecodeHeaders_should_reject_single_header_exceeding_max_size() @@ -322,8 +308,6 @@ public void DecodeHeaders_should_reject_total_headers_exceeding_max_total_size() Assert.Contains("128", ex.Message); } - #endregion - private byte[] EncodeHeaders(List headers) { using var owner = System.Buffers.MemoryPool.Shared.Rent(4096); @@ -338,4 +322,4 @@ private static StreamState BuildStreamState(byte[] headerBlock) state.AppendHeader(headerBlock); return state; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Security/Http2ServerSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Security/Http2ServerSecuritySpec.cs index 62559a2a3..ffa6944c8 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Security/Http2ServerSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Decoder/Security/Http2ServerSecuritySpec.cs @@ -24,14 +24,12 @@ public sealed class Http2ServerSecuritySpec { private readonly HpackEncoder _encoder = new(useHuffman: false); - #region Header Size Limits (RFC 9113 §10.5.1) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-10.5.1")] public void Hpack_bomb_should_be_rejected_by_header_size_limit() { // Test: single header with size exceeding maxHeaderSize (256 bytes) - var maxHeaderSize = 256; + const int maxHeaderSize = 256; var decoder = new Http2ServerDecoder(maxHeaderSize: maxHeaderSize); // Create a header with a 300-byte value to exceed the limit @@ -61,7 +59,7 @@ public void Hpack_bomb_should_be_rejected_by_header_size_limit() public void Many_small_headers_exceeding_total_size_should_be_rejected() { // Test: many small headers that individually pass but collectively exceed maxTotalHeaderSize (256 bytes) - var maxTotalHeaderSize = 256; + const int maxTotalHeaderSize = 256; var decoder = new Http2ServerDecoder(maxTotalHeaderSize: maxTotalHeaderSize); var headers = new List @@ -94,10 +92,6 @@ public void Many_small_headers_exceeding_total_size_should_be_rejected() Assert.Contains("RFC 9113", ex.Message); } - #endregion - - #region Header Field Name Validation (RFC 9113 §8.2.1, §10.3) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-8.2.1")] public void Uppercase_header_name_should_be_rejected() @@ -125,32 +119,6 @@ public void Uppercase_header_name_should_be_rejected() Assert.Contains("RFC 9113", ex.Message); } - [Fact(Timeout = 5000)] - [Trait("RFC", "RFC9113-10.3")] - public void Empty_header_name_should_be_rejected() - { - // NOTE: This test is SKIPPED because HpackEncoder enforces empty header name validation - // at the encoder level (RFC 7541 §7.2 violation in HpackEncoder.Encode()). - // The FieldValidator in the decoder is still the second line of defense. - // This is an acceptable defense-in-depth design. - - // The actual test would be: - // Test: empty header name (not a valid token per RFC 9113 §10.3) - var decoder = new Http2ServerDecoder(); - - // Headers with empty name are rejected at the encoder level: - // new("", "value") → HpackException: "RFC 7541 §7.2 violation: empty header name is not allowed." - // - // A decoder test cannot inject an empty header name directly because the encoder - // blocks it. This validates that we have defense-in-depth, with the encoder as - // the primary gate and the FieldValidator as the secondary gate for any - // hand-crafted wire data. - } - - #endregion - - #region Header Field Value Validation (RFC 9113 §10.3) - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-10.3")] public void Header_value_with_null_byte_should_be_rejected() @@ -178,8 +146,6 @@ public void Header_value_with_null_byte_should_be_rejected() Assert.Contains("RFC 9113", ex.Message); } - #endregion - private byte[] EncodeHeaders(List headers) { using var owner = System.Buffers.MemoryPool.Shared.Rent(4096); @@ -194,4 +160,4 @@ private static StreamState BuildStreamState(byte[] headerBlock) state.AppendHeader(headerBlock); return state; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerEncoderFragmentationSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerEncoderFragmentationSpec.cs index cb2c97e15..e78fe7538 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerEncoderFragmentationSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerEncoderFragmentationSpec.cs @@ -2,6 +2,7 @@ using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Encoder; @@ -22,7 +23,7 @@ public void EncodeHeaders_exceeding_MaxFrameSize_should_produce_CONTINUATION_fra // Add multiple headers to ensure the encoded block exceeds MaxFrameSize for (var i = 0; i < 10; i++) { - ctx.Response.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; + ctx.Get()?.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; } // Act @@ -47,7 +48,7 @@ public void EncodeHeaders_CONTINUATION_frames_should_not_carry_EndStream() var ctx = ServerTestContext.CreateResponse(); for (var i = 0; i < 10; i++) { - ctx.Response.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; + ctx.Get()?.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; } // Act @@ -75,7 +76,7 @@ public void EncodeHeaders_only_last_CONTINUATION_has_EndHeaders() var ctx = ServerTestContext.CreateResponse(); for (var i = 0; i < 10; i++) { - ctx.Response.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; + ctx.Get()?.Headers[$"x-header-{i}"] = $"this-is-a-long-header-value-to-force-fragmentation-{i}"; } // Act @@ -120,11 +121,11 @@ public void EncodeHeaders_fragmented_headers_should_decode_correctly() _encoder.ApplyClientSettings([(SettingsParameter.MaxFrameSize, 64u)]); var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["x-custom-header"] = "custom-value"; - ctx.Response.Headers["x-another-header"] = "another-value"; + ctx.Get()?.Headers["x-custom-header"] = "custom-value"; + ctx.Get()?.Headers["x-another-header"] = "another-value"; for (var i = 0; i < 8; i++) { - ctx.Response.Headers[$"x-header-{i}"] = $"header-value-{i}"; + ctx.Get()?.Headers[$"x-header-{i}"] = $"header-value-{i}"; } // Act @@ -166,7 +167,7 @@ public void ResetHpack_should_clear_encoder_state() { // Arrange var ctx1 = ServerTestContext.CreateResponse(); - ctx1.Response.Headers["x-test"] = "value1"; + ctx1.Get()?.Headers["x-test"] = "value1"; // Encode first response var frames1 = _encoder.EncodeHeaders(ctx1, streamId: 1, hasBody: false); @@ -177,7 +178,7 @@ public void ResetHpack_should_clear_encoder_state() // Encode second response after reset var ctx2 = ServerTestContext.CreateResponse(201); - ctx2.Response.Headers["x-test"] = "value2"; + ctx2.Get()?.Headers["x-test"] = "value2"; var frames2 = _encoder.EncodeHeaders(ctx2, streamId: 3, hasBody: false); // Assert: No crash occurred and frames were produced @@ -191,3 +192,4 @@ public void ResetHpack_should_clear_encoder_state() Assert.Equal("201", statusHeader.Value); } } + diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseBufferSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseBufferSpec.cs index 859f9e097..05c746144 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseBufferSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseBufferSpec.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; @@ -9,24 +8,8 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Encoder; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine response body streaming and flow control. -/// Tests response header encoding, timer-driven body draining, and WINDOW_UPDATE handling. -/// public sealed class Http2ServerResponseBufferSpec { - private static TurboHttpContext CreateResponseContext() - { - var features = new TurboFeatureCollection(); - features.Set(new TurboHttpRequestFeature()); - features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); - } - - private static byte[] BuildHeadersFrame(int streamId, ReadOnlyMemory headerBlock, bool endStream = false, bool endHeaders = true) { @@ -101,7 +84,7 @@ private static ReadOnlyMemory EncodeHeaders(string method, string path, st return new Memory(buffer, 0, written); } - private static void DecodeFramesAsStream(FakeServerOps ops, Http2ServerStateMachine sm, byte[] frameData) + private static void DecodeFramesAsStream(Http2ServerStateMachine sm, byte[] frameData) { var buffer = TransportBuffer.Rent(frameData.Length); frameData.CopyTo(buffer.FullMemory.Span); @@ -136,7 +119,7 @@ public void OnResponse_with_no_body_should_send_headers_with_endstream() // Send HEADERS frame for stream 1 var headerBlock = EncodeHeaders("GET", "/api/status", "example.com"); var headersFrameData = BuildHeadersFrame(streamId: 1, headerBlock, endStream: true, endHeaders: true); - DecodeFramesAsStream(ops, sm, headersFrameData); + DecodeFramesAsStream(sm, headersFrameData); Assert.Single(ops.Requests); @@ -144,8 +127,8 @@ public void OnResponse_with_no_body_should_send_headers_with_endstream() // Send response var requestContext = ops.Requests[0]; - requestContext.Response.StatusCode = 200; - requestContext.Response.ContentLength = 0; + requestContext.Get()?.StatusCode = 200; + requestContext.Get()?.Headers["Content-Length"] = "0"; sm.OnResponse(requestContext); // Extract frames after response @@ -168,7 +151,7 @@ public void OnResponse_with_body_should_schedule_drain_timer_and_not_set_endstre // Send HEADERS frame for stream 1 var headerBlock = EncodeHeaders("GET", "/api/data", "example.com"); var headersFrameData = BuildHeadersFrame(streamId: 1, headerBlock, endStream: true, endHeaders: true); - DecodeFramesAsStream(ops, sm, headersFrameData); + DecodeFramesAsStream(sm, headersFrameData); Assert.Single(ops.Requests); @@ -176,8 +159,8 @@ public void OnResponse_with_body_should_schedule_drain_timer_and_not_set_endstre // Send response var requestContext = ops.Requests[0]; - requestContext.Response.StatusCode = 200; - requestContext.Response.ContentLength = 100; + requestContext.Get()?.StatusCode = 200; + requestContext.Get()?.Headers["Content-Length"] = "100"; sm.OnResponse(requestContext); // Extract frames after response @@ -198,16 +181,16 @@ public void WindowUpdate_should_drain_outbound_buffer() var headerBlock = EncodeHeaders("GET", "/api/data", "example.com"); var headersFrameData = BuildHeadersFrame(streamId: 1, headerBlock, endStream: true, endHeaders: true); - DecodeFramesAsStream(ops, sm, headersFrameData); + DecodeFramesAsStream(sm, headersFrameData); Assert.Single(ops.Requests); var requestContext = ops.Requests[0]; - requestContext.Response.StatusCode = 200; + requestContext.Get()?.StatusCode = 200; sm.OnResponse(requestContext); var windowUpdateData = BuildWindowUpdateFrame(streamId: 1, increment: 50000); - DecodeFramesAsStream(ops, sm, windowUpdateData); + DecodeFramesAsStream(sm, windowUpdateData); } [Fact(Timeout = 5000)] @@ -265,7 +248,4 @@ public void ServerResponseEncoder_ApplyClientSettings_should_ignore_initial_wind // MaxFrameSize should remain unchanged Assert.Equal(16384, encoder.MaxFrameSize); } -} - - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseEncoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseEncoderSpec.cs index c34497e56..472127f3c 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseEncoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseEncoderSpec.cs @@ -1,7 +1,8 @@ -using TurboHTTP.Protocol.Syntax.Http2; +using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Encoder; @@ -41,7 +42,7 @@ public void EncodeHeaders_with_body_flag_returns_HeadersFrame_without_endStream( public void EncodeHeaders_response_headers_are_HPACK_encoded() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["x-custom-header"] = "test-value"; + ctx.Get()?.Headers["x-custom-header"] = "test-value"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -61,7 +62,7 @@ public void EncodeHeaders_response_headers_are_HPACK_encoded() public void EncodeHeaders_status_pseudo_header_is_first() { var ctx = ServerTestContext.CreateResponse(201); - ctx.Response.Headers["x-first"] = "value"; + ctx.Get()?.Headers["x-first"] = "value"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -76,9 +77,9 @@ public void EncodeHeaders_status_pseudo_header_is_first() public void EncodeHeaders_filters_forbidden_headers() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["connection"] = "close"; - ctx.Response.Headers["transfer-encoding"] = "chunked"; - ctx.Response.Headers["x-allowed"] = "yes"; + ctx.Get()?.Headers["connection"] = "close"; + ctx.Get()?.Headers["transfer-encoding"] = "chunked"; + ctx.Get()?.Headers["x-allowed"] = "yes"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -110,8 +111,8 @@ public void EncodeHeaders_204_NoContent_no_body_returns_endStream_true() public void EncodeHeaders_response_with_content_headers() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["content-type"] = "application/json"; - ctx.Response.Headers["content-length"] = "4"; + ctx.Get()?.Headers["content-type"] = "application/json"; + ctx.Get()?.Headers["content-length"] = "4"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -130,7 +131,7 @@ public void EncodeHeaders_response_with_content_headers() public void EncodeHeaders_response_headers_are_lowercase() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["X-Custom-Header"] = "value"; + ctx.Get()?.Headers["X-Custom-Header"] = "value"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -155,4 +156,4 @@ public void EncodeHeaders_multiple_responses_reuses_lists() Assert.Single(frames1); Assert.Single(frames2); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseFrameSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseFrameSpec.cs index 926278ba7..10afff654 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseFrameSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Encoder/Http2ServerResponseFrameSpec.cs @@ -1,7 +1,8 @@ -using TurboHTTP.Protocol.Syntax.Http2; +using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Encoder; @@ -43,8 +44,8 @@ public void EncodeHeaders_status_pseudo_header_present_in_HPACK_block() public void EncodeHeaders_status_pseudo_header_is_first_in_header_block() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["x-first"] = "value"; - ctx.Response.Headers["x-second"] = "value"; + ctx.Get()?.Headers["x-first"] = "value"; + ctx.Get()?.Headers["x-second"] = "value"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -99,9 +100,9 @@ public void EncodeHeaders_no_body_sets_endStream() public void EncodeHeaders_filters_forbidden_connection_specific_headers() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["connection"] = "close"; - ctx.Response.Headers["transfer-encoding"] = "chunked"; - ctx.Response.Headers["x-allowed"] = "yes"; + ctx.Get()?.Headers["connection"] = "close"; + ctx.Get()?.Headers["transfer-encoding"] = "chunked"; + ctx.Get()?.Headers["x-allowed"] = "yes"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -121,8 +122,8 @@ public void EncodeHeaders_filters_forbidden_connection_specific_headers() public void EncodeHeaders_header_names_lowercased() { var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["X-Custom-Header"] = "value"; - ctx.Response.Headers["X-Another-Header"] = "another"; + ctx.Get()?.Headers["X-Custom-Header"] = "value"; + ctx.Get()?.Headers["X-Another-Header"] = "another"; var frames = _encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); var headersFrame = Assert.IsType(frames[0]); @@ -134,4 +135,4 @@ public void EncodeHeaders_header_names_lowercased() Assert.Equal("x-custom-header", customHeader.Name); Assert.Equal("x-another-header", anotherHeader.Name); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Http2ServerTrailerEncodingSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Http2ServerTrailerEncodingSpec.cs index ddf748d44..c52e73537 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Http2ServerTrailerEncodingSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Http2ServerTrailerEncodingSpec.cs @@ -1,11 +1,9 @@ -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Adapters; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; +using TurboHTTP.Server.Context; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server; @@ -46,25 +44,26 @@ public void TrailerFeature_should_reject_prohibited_trailer_fields() [Fact(Timeout = 5000)] [Trait("RFC", "RFC9113-8.1")] - public void TurboHttpResponse_should_expose_DeclareTrailer_and_AppendTrailer() + public void ResponseTrailersFeature_should_store_and_expose_trailers() { var features = new TurboFeatureCollection(); + features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature()); - features.Set(new TurboHttpResponseTrailersFeature()); + var trailersFeature = new TurboHttpResponseTrailersFeature(); + features.Set(trailersFeature); - var response = new TurboHttpResponse(features); - var httpContext = new DefaultHttpContext(features); - response.SetHttpContext(httpContext); + // Set trailers directly on the feature + trailersFeature.Trailers["grpc-status"] = "0"; + trailersFeature.Trailers["grpc-message"] = "OK"; - response.DeclareTrailer("grpc-status"); - response.AppendTrailer("grpc-status", "0"); - response.AppendTrailer("grpc-message", "OK"); + // Verify trailers are stored + Assert.Equal("0", trailersFeature.Trailers["grpc-status"]); + Assert.Equal("OK", trailersFeature.Trailers["grpc-message"]); - var trailers = response.GetTrailers(); - - Assert.Equal("0", trailers["grpc-status"]); - Assert.Equal("OK", trailers["grpc-message"]); - Assert.Contains("grpc-status", response.Headers["Trailer"].ToString()); + // Verify we can retrieve them via the feature + var retrieved = features.Get(); + Assert.NotNull(retrieved); + Assert.Equal("0", retrieved.Trailers["grpc-status"]); } [Fact(Timeout = 5000)] @@ -129,5 +128,4 @@ public void Encoder_should_filter_prohibited_trailer_fields() Assert.DoesNotContain(decodedHeaders, h => h.Name == "transfer-encoding"); Assert.DoesNotContain(decodedHeaders, h => h.Name == "content-length"); } -} - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2ContinuationStateSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2ContinuationStateSpec.cs index 2cee82ce7..115ad0a05 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2ContinuationStateSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2ContinuationStateSpec.cs @@ -4,17 +4,14 @@ using TurboHTTP.Protocol.Syntax.Http2.Options; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; +using TurboHTTP.Server; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.SessionManager; -/// -/// Unit tests for HTTP/2 SessionManager CONTINUATION state machine. -/// Tests fragmented header blocks, timer management, and stream correlation. -/// public sealed class Http2ContinuationStateSpec { - private static byte[] BuildHeadersFrame( int streamId, ReadOnlyMemory headerBlock, @@ -107,7 +104,8 @@ public void Headers_without_EndHeaders_then_Continuation_should_emit_request() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -142,7 +140,7 @@ public void Headers_without_EndHeaders_then_Continuation_should_emit_request() // Now request should be emitted Assert.Single(ops.Requests); var context = ops.Requests[0]; - Assert.Equal("GET", context.Request.Method); + Assert.Equal("GET", context.Get()?.Method); } [Fact(Timeout = 5000)] @@ -152,7 +150,8 @@ public void Continuation_on_wrong_stream_should_throw_protocol_error() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -177,10 +176,7 @@ public void Continuation_on_wrong_stream_should_throw_protocol_error() // RFC 9113 §6.10 requires a CONTINUATION on the same stream // The FrameDecoder catches this before SessionManager processing - var ex = Assert.Throws(() => - { - sm.DecodeClientData(WrapFrame(continuationFrame)); - }); + var ex = Assert.Throws(() => { sm.DecodeClientData(WrapFrame(continuationFrame)); }); Assert.Contains("RFC 9113 §6.10", ex.Message); Assert.Contains("stream", ex.Message); @@ -193,7 +189,8 @@ public void Headers_with_EndHeaders_true_should_emit_request_immediately() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -212,7 +209,7 @@ public void Headers_with_EndHeaders_true_should_emit_request_immediately() // Request should be emitted immediately Assert.Single(ops.Requests); var context = ops.Requests[0]; - Assert.Equal("GET", context.Request.Method); + Assert.Equal("GET", context.Get()?.Method); // No timer should be scheduled (END_HEADERS was set) Assert.Empty(ops.ScheduledTimers); @@ -225,7 +222,8 @@ public void Headers_without_EndHeaders_should_schedule_headers_timeout() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.ScheduledTimers.Clear(); @@ -259,7 +257,8 @@ public void Continuation_with_EndHeaders_should_cancel_headers_timeout() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.ScheduledTimers.Clear(); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2FlowControlEnforcementSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2FlowControlEnforcementSpec.cs index 176b0e592..8dd84106e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2FlowControlEnforcementSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2FlowControlEnforcementSpec.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Options; @@ -8,28 +7,10 @@ using TurboHTTP.Server; using TurboHTTP.Tests.Shared; - namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.SessionManager; -/// -/// Unit tests for HTTP/2 SessionManager flow control enforcement. -/// Tests WINDOW_UPDATE on stream 0, DATA on closed streams, and empty DATA with END_STREAM. -/// public sealed class Http2FlowControlEnforcementSpec { - private static TurboHttpContext CreateResponseContext(long streamId) - { - var features = new TurboFeatureCollection(); - features.Set(new TurboHttpRequestFeature()); - features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - features.Set(new TurboStreamIdFeature(streamId)); - return new TurboHttpContext(features); - } - - private static byte[] BuildHeadersFrame(int streamId, bool endStream = false) { var encoder = new HpackEncoder(useHuffman: false); @@ -115,7 +96,8 @@ public void WindowUpdate_on_stream_0_should_not_crash() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -136,7 +118,8 @@ public void Data_on_closed_stream_should_emit_RstStream() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -149,12 +132,10 @@ public void Data_on_closed_stream_should_emit_RstStream() // Request should be emitted Assert.Single(ops.Requests); var requestContext = ops.Requests[0]; - var requestStreamIdFeature = requestContext.Features.Get(); - var streamId = requestStreamIdFeature?.StreamId ?? 1; - // Step 2: Send response with ContentLength=0 to close the stream - requestContext.Response.StatusCode = 200; - requestContext.Response.ContentLength = 0; + // Step 2: Send response with no body to close the stream + requestContext.Get()?.StatusCode = 200; + requestContext.Get()?.Headers["Content-Length"] = "0"; sm.OnResponse(requestContext); // Stream 1 should be closed after response with no body @@ -198,7 +179,8 @@ public void Empty_data_with_EndStream_should_complete_request_body() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -225,4 +207,4 @@ public void Empty_data_with_EndStream_should_complete_request_body() // Request should still be the same Assert.Equal(request, ops.Requests[0]); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2SettingsGoawaySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2SettingsGoawaySpec.cs index c7e3487f3..fcb04a49e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2SettingsGoawaySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2SettingsGoawaySpec.cs @@ -2,23 +2,19 @@ using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Options; using TurboHTTP.Protocol.Syntax.Http2.Server; +using TurboHTTP.Server; using TurboHTTP.Tests.Shared; - namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.SessionManager; -/// -/// Unit tests for HTTP/2 SessionManager SETTINGS and GOAWAY handling. -/// Tests frame emission for SETTINGS ACK, PING ACK, and GOAWAY processing. -/// RFC 9113 §6.5 (SETTINGS), §6.7 (PING), §6.8 (GOAWAY). -/// public sealed class Http2SettingsGoawaySpec { private static Http2ServerSessionManager CreateSessionManager(FakeServerOps ops) { var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - return new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + return new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); } private static byte[] BuildSettingsFrame(bool isAck = false) @@ -144,7 +140,7 @@ public void Ping_should_emit_ack_with_echoed_data() ops.Outbound.Clear(); // Send PING with 8 bytes of data - byte[] pingData = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + byte[] pingData = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; var pingFrame = BuildPingFrame(pingData, isAck: false); sm.DecodeClientData(WrapFrame(pingFrame)); @@ -176,7 +172,7 @@ public void Ping_ack_should_be_ignored() ops.Outbound.Clear(); // Send PING with ACK already set - byte[] pingData = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + byte[] pingData = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; var pingFrame = BuildPingFrame(pingData, isAck: true); sm.DecodeClientData(WrapFrame(pingFrame)); @@ -212,10 +208,13 @@ public void PreStart_should_emit_settings_with_configured_stream_window_size() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var customStreamWindow = 256 * 1024; + const int customStreamWindow = 256 * 1024; + var options = new TurboServerOptions + { + Http2 = { InitialStreamWindowSize = customStreamWindow } + }; var sessionManager = new Http2ServerSessionManager( - encoderOptions, decoderOptions, ops, - initialStreamWindowSize: customStreamWindow); + encoderOptions, decoderOptions, ops, options); sessionManager.PreStart(); @@ -238,6 +237,7 @@ public void PreStart_should_emit_settings_with_configured_stream_window_size() Assert.Equal((uint)customStreamWindow, value); found = true; } + offset += 6; } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2StreamLifecycleSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2StreamLifecycleSpec.cs index 2c7573614..b2c541aae 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2StreamLifecycleSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/SessionManager/Http2StreamLifecycleSpec.cs @@ -1,35 +1,29 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Options; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; - namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.SessionManager; -/// -/// Unit tests for HTTP/2 SessionManager stream lifecycle and max concurrent streams. -/// Tests stream creation, closure, RST_STREAM handling, and max concurrent stream limits. -/// public sealed class Http2StreamLifecycleSpec { - private static TurboHttpContext CreateResponseContext(long streamId = 99) + private static IFeatureCollection CreateResponseContext(long streamId = 99) { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); + features.Set(bodyFeature); features.Set(new TurboStreamIdFeature(streamId)); - return new TurboHttpContext(features); + return features; } - private static byte[] BuildHeadersFrame(int streamId, bool endStream = false) { var encoder = new HpackEncoder(useHuffman: false); @@ -99,7 +93,9 @@ public void Should_accept_streams_up_to_max_concurrent() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions { MaxConcurrentStreams = 2 }; - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -129,7 +125,9 @@ public void Should_refuse_stream_above_max_concurrent() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions { MaxConcurrentStreams = 1 }; - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); // Clear initial SETTINGS frame @@ -180,7 +178,9 @@ public void RstStream_on_active_stream_should_close_it() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -209,7 +209,8 @@ public void RstStream_on_closed_stream_should_not_crash() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -233,7 +234,8 @@ public void Headers_with_EndStream_true_should_emit_request_immediately() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -248,7 +250,7 @@ public void Headers_with_EndStream_true_should_emit_request_immediately() var context = ops.Requests[0]; // Request should have stream ID set - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(1, streamIdFeature.StreamId); } @@ -259,7 +261,8 @@ public void Cleanup_should_be_idempotent() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -287,7 +290,8 @@ public void OnResponse_for_unknown_stream_should_not_crash() var ops = new FakeServerOps(); var encoderOptions = new Http2ServerEncoderOptions(); var decoderOptions = new Http2ServerDecoderOptions(); - var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops); + var options = new TurboServerOptions(); + var sm = new Http2ServerSessionManager(encoderOptions, decoderOptions, ops, options); sm.PreStart(); ops.Outbound.Clear(); @@ -299,4 +303,4 @@ public void OnResponse_for_unknown_stream_should_not_crash() // No crash, test passes } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerSettingsSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerSettingsSpec.cs index 10b86c2f3..be308e317 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerSettingsSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerSettingsSpec.cs @@ -1,6 +1,7 @@ -using TurboHTTP.Protocol.Syntax.Http2; +using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.StateMachine; @@ -33,7 +34,7 @@ public void ApplyClientSettings_updates_header_table_size() // Verify settings applied without exception var ctx = ServerTestContext.CreateResponse(); - ctx.Response.Headers["x-test"] = "value"; + ctx.Get()?.Headers["x-test"] = "value"; var frames = encoder.EncodeHeaders(ctx, streamId: 1, hasBody: false); Assert.NotEmpty(frames); @@ -55,7 +56,7 @@ public void ResetHpack_allows_encoder_reuse() var encoder = new Http2ServerEncoder(); var ctx1 = ServerTestContext.CreateResponse(); - ctx1.Response.Headers["x-header"] = "value1"; + ctx1.Get()?.Headers["x-header"] = "value1"; var frames1 = encoder.EncodeHeaders(ctx1, streamId: 1, hasBody: false); Assert.NotEmpty(frames1); @@ -63,9 +64,9 @@ public void ResetHpack_allows_encoder_reuse() encoder.ResetHpack(); var ctx2 = ServerTestContext.CreateResponse(); - ctx2.Response.Headers["x-header"] = "value2"; + ctx2.Get()?.Headers["x-header"] = "value2"; var frames2 = encoder.EncodeHeaders(ctx2, streamId: 3, hasBody: false); Assert.NotEmpty(frames2); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStateMachineSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStateMachineSpec.cs index 8965d4598..232041249 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStateMachineSpec.cs @@ -1,32 +1,16 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.StateMachine; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine. -/// Tests frame decoding, request assembly, response encoding, and flow control. -/// public sealed class Http2ServerStateMachineSpec { - private static TurboHttpContext CreateResponseContext() - { - var features = new TurboFeatureCollection(); - features.Set(new TurboHttpRequestFeature()); - features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); - } - - private static byte[] BuildHeadersFrame(int streamId, ReadOnlyMemory headerBlock, bool endStream = false, bool endHeaders = true) { @@ -59,8 +43,7 @@ private static byte[] BuildHeadersFrame(int streamId, ReadOnlyMemory heade private static byte[] BuildSettingsFrame(bool isAck = false) { const int frameHeaderSize = 9; - var frameSize = frameHeaderSize; - var frame = new byte[frameSize]; + var frame = new byte[frameHeaderSize]; frame[0] = 0; frame[1] = 0; @@ -79,7 +62,7 @@ private static byte[] BuildPingFrame(bool isAck = false) { const int frameHeaderSize = 9; const int pingDataSize = 8; - var frameSize = frameHeaderSize + pingDataSize; + const int frameSize = frameHeaderSize + pingDataSize; var frame = new byte[frameSize]; frame[0] = 0; @@ -128,8 +111,9 @@ public void PreStart_should_emit_settings_frame() sm.PreStart(); - Assert.Single(ops.Outbound); + Assert.Equal(2, ops.Outbound.Count); Assert.IsType(ops.Outbound[0]); + Assert.IsType(ops.Outbound[1]); } [Fact(Timeout = 5000)] @@ -152,13 +136,13 @@ public void DecodeClientData_with_headers_should_produce_request_with_stream_id( var context = ops.Requests[0]; // Verify stream ID was stored in request features - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(1, streamIdFeature.StreamId); // Verify request properties - Assert.Equal("GET", context.Request.Method); - Assert.Equal("/", context.Request.Path.Value); + Assert.Equal("GET", context.Get()?.Method); + Assert.Equal("/", context.Get()?.Path); } [Fact(Timeout = 5000)] @@ -267,7 +251,7 @@ public void OnResponse_should_encode_and_emit_frames() // Now send a response ops.Outbound.Clear(); var requestContext = ops.Requests[0]; - requestContext.Response.StatusCode = 200; + requestContext.Get()?.StatusCode = 200; sm.OnResponse(requestContext); // Should emit response frames @@ -311,7 +295,4 @@ public void Cleanup_should_dispose_decoder() // Should not throw sm.Cleanup(); } -} - - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStreamCorrelationSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStreamCorrelationSpec.cs index bcb70cb23..0e038d270 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStreamCorrelationSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerStreamCorrelationSpec.cs @@ -1,33 +1,28 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.StateMachine; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine stream correlation. -/// Tests handling of multiple concurrent streams and correct response routing via stream IDs. -/// public sealed class Http2ServerStreamCorrelationSpec { - private static TurboHttpContext CreateResponseContext(long streamId) + private static IFeatureCollection CreateResponseContext(long streamId) { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature { StatusCode = 200 }); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); + features.Set(bodyFeature); features.Set(new TurboStreamIdFeature(streamId)); - return new TurboHttpContext(features); + return features; } - private static ReadOnlyMemory EncodeHeaders(string method, string path, string authority = "localhost") { var encoder = new HpackEncoder(useHuffman: true); @@ -106,16 +101,16 @@ public void Multiple_concurrent_streams_should_correlate_responses_to_correct_st // Verify stream IDs are correctly stored in request features var context1 = ops.Requests[0]; - var streamIdFeature1 = context1.Features.Get(); + var streamIdFeature1 = context1.Get(); Assert.NotNull(streamIdFeature1); Assert.Equal(1, streamIdFeature1.StreamId); - Assert.Equal("/path1", context1.Request.Path.Value); + Assert.Equal("/path1", context1.Get()?.Path); var context3 = ops.Requests[1]; - var streamIdFeature3 = context3.Features.Get(); + var streamIdFeature3 = context3.Get(); Assert.NotNull(streamIdFeature3); Assert.Equal(3, streamIdFeature3.StreamId); - Assert.Equal("/path3", context3.Request.Path.Value); + Assert.Equal("/path3", context3.Get()?.Path); // Now respond to stream 3 first ops.Outbound.Clear(); @@ -205,10 +200,10 @@ public void Stream_IDs_should_preserve_request_response_correlation_across_inter var expectedStreamId = 1 + (i * 2); var expectedPath = $"/path{expectedStreamId}"; - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(expectedStreamId, streamIdFeature.StreamId); - Assert.Equal(expectedPath, context.Request.Path.Value); + Assert.Equal(expectedPath, context.Get()?.Path); } // Respond in reverse order (5, 3, 1) and verify correct stream IDs are used @@ -218,7 +213,7 @@ public void Stream_IDs_should_preserve_request_response_correlation_across_inter foreach (var idx in responseOrder) { var reqContext = ops.Requests[idx]; - var reqStreamIdFeature = reqContext.Features.Get(); + var reqStreamIdFeature = reqContext.Get(); var reqStreamId = reqStreamIdFeature?.StreamId ?? 0; ops.Outbound.Clear(); @@ -288,7 +283,7 @@ public void Concurrent_streams_should_maintain_independent_state() var streamIds = ops.Requests .Select(ctx => { - var feature = ctx.Features.Get(); + var feature = ctx.Get(); return (int)(feature?.StreamId ?? 0); }) .OrderBy(id => id) @@ -297,11 +292,8 @@ public void Concurrent_streams_should_maintain_independent_state() Assert.Equal(new[] { 1, 3, 5 }, streamIds); // Verify paths match stream order - Assert.Equal("/", ops.Requests[0].Request.Path.Value); - Assert.Equal("/submit", ops.Requests[1].Request.Path.Value); - Assert.Equal("/status", ops.Requests[2].Request.Path.Value); + Assert.Equal("/", ops.Requests[0].Get()?.Path); + Assert.Equal("/submit", ops.Requests[1].Get()?.Path); + Assert.Equal("/status", ops.Requests[2].Get()?.Path); } -} - - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerTimerErrorSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerTimerErrorSpec.cs index 3dbbfaa46..3718e88ef 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerTimerErrorSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/StateMachine/Http2ServerTimerErrorSpec.cs @@ -1,21 +1,17 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http2; using TurboHTTP.Protocol.Syntax.Http2.Hpack; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.StateMachine; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine timer behavior and error recovery. -/// Tests keep-alive timers, header timeouts, connection cleanup, and edge cases. -/// public sealed class Http2ServerTimerErrorSpec { - private static TurboHttpContext CreateResponseContext(long streamId = 999) + private static IFeatureCollection CreateResponseContext(long streamId = 999) { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); @@ -23,11 +19,10 @@ private static TurboHttpContext CreateResponseContext(long streamId = 999) features.Set(new TurboStreamIdFeature(streamId)); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } - private static byte[] BuildHeadersFrame(int streamId, bool endStream = true) { var encoder = new HpackEncoder(useHuffman: false); @@ -198,8 +193,4 @@ public void OnResponse_for_unknown_stream_should_not_crash() var context = CreateResponseContext(); sm.OnResponse(context); } -} - - - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerBodyStreamingSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerBodyStreamingSpec.cs index f860fd721..780282f19 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerBodyStreamingSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerBodyStreamingSpec.cs @@ -4,16 +4,12 @@ using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; using TurboHTTP.Tests.Shared; +using Microsoft.AspNetCore.Http.Features; namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Streaming; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine streaming request body handling via System.IO.Pipelines. -/// Tests PipeBodyContent emission, DATA frame writing, and max body size enforcement. -/// public sealed class Http2ServerBodyStreamingSpec { - private static byte[] BuildHeadersFrame(int streamId, ReadOnlyMemory headerBlock, bool endStream = false, bool endHeaders = true) { @@ -108,7 +104,7 @@ public async Task DecodeClientData_with_body_should_emit_request_on_headers_with var context = ops.Requests[0]; // Request should have a body stream - var bodyStream = context.Request.Body; + var bodyStream = context.Get()?.Body; Assert.NotNull(bodyStream); Assert.True(bodyStream.CanRead); @@ -151,7 +147,7 @@ public void DecodeClientData_headers_only_should_emit_request_without_pipe_conte var context = ops.Requests[0]; // Request should have empty body stream - var bodyStream = context.Request.Body; + var bodyStream = context.Get()?.Body; Assert.NotNull(bodyStream); using var ms = new MemoryStream(); bodyStream.CopyTo(ms); @@ -232,7 +228,7 @@ public async Task DecodeClientData_with_multiple_data_frames_should_aggregate_in Assert.Single(ops.Requests); var context = ops.Requests[0]; - var bodyStream = context.Request.Body; + var bodyStream = context.Get()?.Body; Assert.NotNull(bodyStream); // Send first DATA frame @@ -281,7 +277,7 @@ public void DecodeClientData_with_rst_stream_should_complete_body_writer_with_ca Assert.Single(ops.Requests); var context = ops.Requests[0]; - var bodyStream = context.Request.Body; + var bodyStream = context.Get()?.Body; Assert.NotNull(bodyStream); // Send partial DATA frame @@ -322,6 +318,4 @@ public void DecodeClientData_with_rst_stream_should_complete_body_writer_with_ca sm.DecodeClientData(new TransportData(buffer2)); } -} - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerFlowControlSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerFlowControlSpec.cs index 787ec676d..d7ad7b89e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerFlowControlSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerFlowControlSpec.cs @@ -8,14 +8,8 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Streaming; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine flow control behavior. -/// Tests window updates, flow control violations, and stream/connection window management. -/// public sealed class Http2ServerFlowControlSpec { - - private static byte[] BuildDataFrame(int streamId, byte[] data, bool endStream = false) { const int frameHeaderSize = 9; @@ -148,7 +142,7 @@ public void DecodeClientData_with_data_frame_should_emit_window_update_when_thre // Request should be emitted immediately when headers arrive (with endStream=false) Assert.Single(ops.Requests); var context = ops.Requests[0]; - var bodyFeature = context.Features.Get(); + var bodyFeature = context.Get(); Assert.NotNull(bodyFeature); ops.Outbound.Clear(); @@ -302,6 +296,4 @@ public void DecodeClientData_with_multiple_data_frames_should_track_window_corre Assert.True(windowUpdateCount > 0, "Expected at least one WINDOW_UPDATE frame"); } -} - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerTimeoutSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerTimeoutSpec.cs index 07276f054..6593afdf9 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerTimeoutSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Server/Streaming/Http2ServerTimeoutSpec.cs @@ -7,13 +7,8 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Server.Streaming; -/// -/// Unit tests for HTTP/2 Http2ServerStateMachine timeout protection. -/// Tests keep-alive, headers timeout, and body data rate enforcement. -/// public sealed class Http2ServerTimeoutSpec { - private static byte[] BuildHeadersFrame(int streamId, ReadOnlyMemory headerBlock, bool endStream = false, bool endHeaders = true) { @@ -307,6 +302,4 @@ public void Body_rate_check_should_schedule_on_data_frame() Assert.Equal("body-rate-check", rateTimer.Name); Assert.Equal(TimeSpan.FromSeconds(1), rateTimer.Delay); } -} - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http20ConnectionStageSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http20ConnectionStageSpec.cs index 37eb2c30b..28dee3972 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http20ConnectionStageSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http20ConnectionStageSpec.cs @@ -263,7 +263,7 @@ public async Task Http20ConnectionStage_should_complete_on_goaway_with_no_inflig serverSubscription.SendComplete(); // Stage completes when server upstream finishes - await Task.Run(() => networkSub.ExpectComplete(), TestContext.Current.CancellationToken); + networkSub.ExpectComplete(); } [Fact(Timeout = 10_000)] @@ -305,6 +305,6 @@ public async Task Http20ConnectionStage_should_complete_when_app_upstream_finish appSubscription.SendComplete(); // Stage should complete - await Task.Run(() => responseSub.ExpectComplete(), TestContext.Current.CancellationToken); + responseSub.ExpectComplete(); } } \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionFlowControlBatchingSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionFlowControlBatchingSpec.cs index 8b665be81..0b1cb6c90 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionFlowControlBatchingSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionFlowControlBatchingSpec.cs @@ -12,7 +12,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Stages; public sealed class Http2ConnectionFlowControlBatchingSpec : StreamTestBase { private const int DefaultStreamWindow = 65535; - private const int DefaultThreshold = 32767; private async Task<(IReadOnlyList Downstream, IReadOnlyList ServerBound)> RunAsync( int initialWindowSize, diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionStreamAcquireSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionStreamAcquireSpec.cs deleted file mode 100644 index 7b7242936..000000000 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http2/Stages/Http2ConnectionStreamAcquireSpec.cs +++ /dev/null @@ -1,131 +0,0 @@ -using TurboHTTP.Client; -using System.Net; -using Akka; -using Akka.Streams; -using Akka.Streams.Dsl; -using Servus.Akka.Transport; -using TurboHTTP.Protocol.Syntax.Http2; -using TurboHTTP.Streams.Stages.Client; -using TurboHTTP.Tests.Shared; -using static TurboHTTP.Tests.Protocol.Syntax.Http2.Stages.Http2ConnectionTestHelper; - -namespace TurboHTTP.Tests.Protocol.Syntax.Http2.Stages; - -public sealed class Http2ConnectionStreamAcquireSpec : StreamTestBase -{ - private async Task<(IReadOnlyList ServerBound, IReadOnlyList Signals)> - RunWithRequestsAsync( - params (HttpRequestMessage, int)[] requestTuples) - { - var networkSink = Sink.Seq(); - - var graph = RunnableGraph.FromGraph( - GraphDsl.Create(networkSink, - (b, nwSink) => - { - var stage = b.Add(new Http20ClientConnectionStage(new TurboClientOptions - { Http2 = { InitialConnectionWindowSize = 65535 } })); - - // A SETTINGS ACK on InServer is harmless (no ACK reply) and lets - // the inlet complete, which tears down the stage via the default - // onUpstreamFinish on _inServer. - var serverSource = b.Add( - Source.From(FramesToInputs([new SettingsFrame([], isAck: true)])) - .InitialDelay(TimeSpan.FromMilliseconds(200))); - - var requestSource = b.Add(Source.From(requestTuples).Select(r => r.Item1)); - var downstreamSink = - b.Add(Sink.Ignore().MapMaterializedValue(_ => NotUsed.Instance)); - - b.From(serverSource).To(stage.InNetwork); - b.From(stage.OutResponse).To(downstreamSink); - b.From(requestSource).To(stage.InRequest); - b.From(stage.OutNetwork).To(nwSink); - - return ClosedShape.Instance; - })); - - var networkTask = graph.Run(Materializer); - - var networkItems = await networkTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); - - return (DecodeFrames(networkItems, skipPreface: true), ExtractSignals(networkItems)); - } - - private async Task<(IReadOnlyList ServerBound, IReadOnlyList Signals)> - RunWithServerAndRequestsAsync( - Http2Frame[] serverFrames, (HttpRequestMessage, int)[] requestTuples, int delayMs = 200) - { - var networkSink = Sink.Seq(); - - var graph = RunnableGraph.FromGraph( - GraphDsl.Create(networkSink, - (b, nwSink) => - { - var stage = b.Add(new Http20ClientConnectionStage(new TurboClientOptions - { Http2 = { InitialConnectionWindowSize = 65535 } })); - - var serverSource = b.Add(Source.From(FramesToInputs(serverFrames))); - var requestSource = b.Add( - Source.From(requestTuples) - .Select(r => r.Item1) - .InitialDelay(TimeSpan.FromMilliseconds(delayMs))); - var downstreamSink = - b.Add(Sink.Ignore().MapMaterializedValue(_ => NotUsed.Instance)); - - b.From(serverSource).To(stage.InNetwork); - b.From(stage.OutResponse).To(downstreamSink); - b.From(requestSource).To(stage.InRequest); - b.From(stage.OutNetwork).To(nwSink); - - return ClosedShape.Instance; - })); - - var networkTask = graph.Run(Materializer); - - var networkItems = await networkTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); - - return (DecodeFrames(networkItems, skipPreface: true), ExtractSignals(networkItems)); - } - - [Fact(Timeout = 10_000)] - [Trait("RFC", "RFC9113-8.1")] - public async Task Http2ConnectionStreamAcquire_should_emit_stream_acquire_item_when_headers_frame_received() - { - var request = (new HttpRequestMessage(HttpMethod.Get, "http://example.com/"), 1); - - var (_, _) = await RunWithRequestsAsync(request); - - // Verify that some control signals are emitted (transport communication) - } - - [Fact(Timeout = 10_000)] - [Trait("RFC", "RFC9113-8.1")] - public async Task Http2ConnectionStreamAcquire_should_not_emit_signal_when_data_frame_received() - { - // A POST request encodes to HeadersFrame + DataFrame. - // Only the HeadersFrame triggers a StreamAcquireItem; the subsequent DATA frame must not. - var request = (new HttpRequestMessage(HttpMethod.Post, "http://example.com/") - { - Content = new ByteArrayContent([0x01]) - }, 1); - - var (_, _) = await RunWithRequestsAsync(request); - - // Verify that control signals are emitted for stream management - } - - [Fact(Timeout = 10_000)] - [Trait("RFC", "RFC9113-8.1")] - public async Task Http2ConnectionStreamAcquire_should_include_correct_key_in_stream_acquire_item_from_pipeline() - { - var request = (new HttpRequestMessage(HttpMethod.Get, "https://example.com/") - { - Version = HttpVersion.Version20 - }, 1); - - var (_, _) = await RunWithRequestsAsync(request); - - // Verify that control signals are emitted (stream endpoint tracking) - } -} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3CookieHeaderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3CookieHeaderSpec.cs index 52057adf0..d101a5b05 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3CookieHeaderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3CookieHeaderSpec.cs @@ -32,11 +32,10 @@ public void ResponseDecoder_should_accept_single_cookie_header() { var tableSync = new QpackTableSync(); var decoder = new Http3ClientDecoder(tableSync); - var frame = new HeadersFrame(tableSync.Encoder.Encode(new[] - { + var frame = new HeadersFrame(tableSync.Encoder.Encode([ (":status", "200"), - ("cookie", "session=abc123"), - })); + ("cookie", "session=abc123") + ])); var state = new StreamState(); decoder.DecodeHeaders(frame, state); Assert.True(state.HasResponse); @@ -48,15 +47,14 @@ public void ResponseDecoder_should_accept_multiple_cookie_headers() { var tableSync = new QpackTableSync(); var decoder = new Http3ClientDecoder(tableSync); - var frame = new HeadersFrame(tableSync.Encoder.Encode(new[] - { + var frame = new HeadersFrame(tableSync.Encoder.Encode([ (":status", "200"), ("cookie", "a=1"), ("cookie", "b=2"), - ("cookie", "c=3"), - })); + ("cookie", "c=3") + ])); var state = new StreamState(); decoder.DecodeHeaders(frame, state); Assert.True(state.HasResponse); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3FrameBatchingSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3FrameBatchingSpec.cs new file mode 100644 index 000000000..1f755cac7 --- /dev/null +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3FrameBatchingSpec.cs @@ -0,0 +1,28 @@ +using Servus.Akka.Transport; +using TurboHTTP.Client; +using TurboHTTP.Protocol.Syntax.Http3.Client; +using TurboHTTP.Protocol.Syntax.Http3.Options; +using TurboHTTP.Tests.Shared; + +namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Client; + +public sealed class Http3FrameBatchingSpec +{ + [Fact(Timeout = 5000)] + public void EncodeRequest_should_emit_single_MultiplexedData_for_headeronly_request() + { + var ops = new FakeOps(); + var encoderOpts = Http3ClientEncoderOptions.Default; + var decoderOpts = Http3ClientDecoderOptions.Default; + var clientOpts = new TurboClientOptions { DangerousAcceptAnyServerCertificate = true }; + + var session = new Http3ClientSessionManager(encoderOpts, decoderOpts, clientOpts, ops); + session.OnTransportConnected(); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/test"); + session.EncodeRequest(request); + + var requestDataItems = ops.Outbound.OfType().Where(md => md.StreamId >= 0).ToList(); + Assert.Single(requestDataItems); + } +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3PseudoHeaderValidationRequestSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3PseudoHeaderValidationRequestSpec.cs index 3fa73e6dc..0e1c70d90 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3PseudoHeaderValidationRequestSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3PseudoHeaderValidationRequestSpec.cs @@ -181,7 +181,7 @@ public void Request_all_pseudo_after_regular_rejected() (":authority", "example.com"), }; - var ex = Assert.Throws(() => Http3ClientEncoder.ValidatePseudoHeaders(headers)); + Assert.Throws(() => Http3ClientEncoder.ValidatePseudoHeaders(headers)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3RequestPathAuthoritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3RequestPathAuthoritySpec.cs index 39f84ffae..534baf176 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3RequestPathAuthoritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3RequestPathAuthoritySpec.cs @@ -26,7 +26,7 @@ public void Encode_should_include_path_with_slash_when_uri_has_no_path() var encoder = CreateEncoder(); var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); var headers = DecodeHeaders(encoder, request); - Assert.Contains(headers, h => h.Name == ":path" && h.Value == "/"); + Assert.Contains(headers, h => h is { Name: ":path", Value: "/" }); } [Fact(Timeout = 5000)] @@ -36,7 +36,7 @@ public void Encode_should_preserve_query_string_in_path() var encoder = CreateEncoder(); var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/search?q=test&page=1"); var headers = DecodeHeaders(encoder, request); - Assert.Contains(headers, h => h.Name == ":path" && h.Value == "/search?q=test&page=1"); + Assert.Contains(headers, h => h is { Name: ":path", Value: "/search?q=test&page=1" }); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3ResponseDecoderEdgeCasesSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3ResponseDecoderEdgeCasesSpec.cs index a4de578ce..3f49c85f8 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3ResponseDecoderEdgeCasesSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3ResponseDecoderEdgeCasesSpec.cs @@ -252,9 +252,9 @@ public void AssembleHeaders_rejects_null_headers() public void AssembleHeaders_empty_header_list() { var state = new StreamState(); - var headers = new List<(string Name, string Value)>(); - var ex = Assert.Throws(() => _decoder.AssembleHeaders(headers, state)); + var ex = Assert.Throws(() => + _decoder.AssembleHeaders(new List<(string Name, string Value)>(), state)); Assert.Contains(":status", ex.Message); } diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3SettingsPopulationSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3SettingsPopulationSpec.cs index ecad0888a..4d4ce3495 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3SettingsPopulationSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/Http3SettingsPopulationSpec.cs @@ -28,7 +28,7 @@ private Http3ClientStateMachine CreateMachine(TurboClientOptions? options = null private static void SimulateConnect(Http3ClientStateMachine sm) { - sm.DecodeServerData(new TransportConnected(default!)); + sm.DecodeServerData(new TransportConnected(null!)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DecoderStreamSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DecoderStreamSpec.cs index 5e8cd34fc..7a0e01428 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DecoderStreamSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DecoderStreamSpec.cs @@ -5,12 +5,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Client.StateMachine; -/// -/// Tests for QPACK decoder stream behavior. -/// These tests verify that decoder state is managed correctly during PreStart() and reconnection cycles. -/// Direct access to internal QPACK state (TableSync, FlushDecoderInstructions) is not available in the public API. -/// Observable behavior (Outbound emissions of MultiplexedData with stream ID -4) is tested instead. -/// public sealed class Http3DecoderStreamSpec { private readonly FakeOps _ops = new(); @@ -22,7 +16,7 @@ private Http3ClientStateMachine CreateMachine(TurboClientOptions? options = null private static void SimulateConnect(Http3ClientStateMachine sm) { - sm.DecodeServerData(new TransportConnected(default!)); + sm.DecodeServerData(new TransportConnected(null!)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DuplicateStreamSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DuplicateStreamSpec.cs index 3cfd8ddb2..8562ba2ad 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DuplicateStreamSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3DuplicateStreamSpec.cs @@ -6,17 +6,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Client.StateMachine; -/// -/// Tests for HTTP/3 stream type uniqueness validation. -/// -/// RFC 9114 §6.2.1 requires that control, encoder, and decoder streams be unique. -/// The new Http3ClientStateMachine API delegates stream type resolution to the internal ProtocolHandler, -/// which is triggered when DecodeServerData receives a ServerStreamAccepted event followed by -/// stream data containing the stream type byte. -/// -/// These tests verify that duplicate stream type declarations are rejected by observing -/// Outbound warnings or connection failures. -/// public sealed class Http3DuplicateStreamSpec { private readonly FakeOps _ops = new(); @@ -46,13 +35,11 @@ public void DecodeServerData_should_accept_control_stream_opening() sm.PreStart(); _ops.Outbound.Clear(); - // Server opens unidirectional stream 1 as control stream - sm.DecodeServerData(new ServerStreamAccepted(1, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(3, StreamDirection.Unidirectional)); var buf = BuildStreamTypeBuffer(StreamType.Control, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf, 1)); + sm.DecodeServerData(new MultiplexedData(buf, 3)); - // Should process without errors Assert.True(true); } @@ -63,17 +50,14 @@ public void DecodeServerData_should_reject_duplicate_control_stream() var sm = CreateMachine(); sm.PreStart(); - // First control stream on QUIC stream 1 - sm.DecodeServerData(new ServerStreamAccepted(1, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(3, StreamDirection.Unidirectional)); var buf1 = BuildStreamTypeBuffer(StreamType.Control, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf1, 1)); + sm.DecodeServerData(new MultiplexedData(buf1, 3)); - // Second control stream on QUIC stream 5 should be rejected - sm.DecodeServerData(new ServerStreamAccepted(5, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(7, StreamDirection.Unidirectional)); var buf2 = BuildStreamTypeBuffer(StreamType.Control, [0x00]); - // This should throw an Http3Exception due to duplicate control stream - var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 5))); + var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 7))); Assert.Contains("Duplicate", ex.Message); } @@ -84,17 +68,14 @@ public void DecodeServerData_should_reject_duplicate_encoder_stream() var sm = CreateMachine(); sm.PreStart(); - // First encoder stream on QUIC stream 1 - sm.DecodeServerData(new ServerStreamAccepted(1, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(3, StreamDirection.Unidirectional)); var buf1 = BuildStreamTypeBuffer(StreamType.QpackEncoder, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf1, 1)); + sm.DecodeServerData(new MultiplexedData(buf1, 3)); - // Second encoder stream on QUIC stream 5 should be rejected - sm.DecodeServerData(new ServerStreamAccepted(5, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(7, StreamDirection.Unidirectional)); var buf2 = BuildStreamTypeBuffer(StreamType.QpackEncoder, [0x00]); - // This should throw an Http3Exception due to duplicate encoder stream - var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 5))); + var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 7))); Assert.Contains("Duplicate", ex.Message); } @@ -105,17 +86,14 @@ public void DecodeServerData_should_reject_duplicate_decoder_stream() var sm = CreateMachine(); sm.PreStart(); - // First decoder stream on QUIC stream 1 - sm.DecodeServerData(new ServerStreamAccepted(1, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(3, StreamDirection.Unidirectional)); var buf1 = BuildStreamTypeBuffer(StreamType.QpackDecoder, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf1, 1)); + sm.DecodeServerData(new MultiplexedData(buf1, 3)); - // Second decoder stream on QUIC stream 5 should be rejected - sm.DecodeServerData(new ServerStreamAccepted(5, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(7, StreamDirection.Unidirectional)); var buf2 = BuildStreamTypeBuffer(StreamType.QpackDecoder, [0x00]); - // This should throw an Http3Exception due to duplicate decoder stream - var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 5))); + var ex = Assert.Throws(() => sm.DecodeServerData(new MultiplexedData(buf2, 7))); Assert.Contains("Duplicate", ex.Message); } @@ -127,17 +105,14 @@ public void DecodeServerData_should_allow_different_critical_stream_types() sm.PreStart(); _ops.Outbound.Clear(); - // Control stream on QUIC stream 1 - sm.DecodeServerData(new ServerStreamAccepted(1, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(3, StreamDirection.Unidirectional)); var buf1 = BuildStreamTypeBuffer(StreamType.Control, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf1, 1)); + sm.DecodeServerData(new MultiplexedData(buf1, 3)); - // Encoder stream on QUIC stream 5 - sm.DecodeServerData(new ServerStreamAccepted(5, StreamDirection.Unidirectional)); + sm.DecodeServerData(new ServerStreamAccepted(7, StreamDirection.Unidirectional)); var buf2 = BuildStreamTypeBuffer(StreamType.QpackEncoder, [0x00]); - sm.DecodeServerData(new MultiplexedData(buf2, 5)); + sm.DecodeServerData(new MultiplexedData(buf2, 7)); - // Should accept both without errors Assert.True(true); } -} \ No newline at end of file +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3GoAwayComplianceSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3GoAwayComplianceSpec.cs index 7fc8f49ac..f46af147d 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3GoAwayComplianceSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3GoAwayComplianceSpec.cs @@ -73,4 +73,4 @@ public void ConnectionState_should_track_idle_timeout() state.RecordActivity(); Assert.False(state.IsIdleTimeoutExpired()); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3StreamLifecycleSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3StreamLifecycleSpec.cs index e9f23ee19..4f63ec6b6 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3StreamLifecycleSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StateMachine/Http3StreamLifecycleSpec.cs @@ -20,15 +20,6 @@ public sealed class Http3StreamLifecycleSpec private Http3ClientStateMachine CreateMachine(FakeOps? ops = null) => new(new TurboClientOptions(), ops ?? _ops); - private static TransportBuffer SerializeFrame(Http3Frame frame) - { - var buffer = TransportBuffer.Rent(frame.SerializedSize); - var span = buffer.FullMemory.Span; - frame.WriteTo(ref span); - buffer.Length = frame.SerializedSize; - return buffer; - } - private static void SimulateConnect(Http3ClientStateMachine sm) => sm.DecodeServerData(new TransportConnected(DummyConnectionInfo)); @@ -86,4 +77,4 @@ public void FrameDecoder_should_decode_data_frame_on_request_stream() var frame = Assert.IsType(result[0]); Assert.Equal(4, frame.Data.Length); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StreamManagerPoolSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StreamManagerPoolSpec.cs new file mode 100644 index 000000000..304934f89 --- /dev/null +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Client/StreamManagerPoolSpec.cs @@ -0,0 +1,35 @@ +using TurboHTTP.Protocol.Syntax.Http3.Client; +using TurboHTTP.Protocol.Syntax.Http3.Qpack; +using TurboHTTP.Tests.Shared; + +namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Client; + +public sealed class StreamManagerPoolSpec +{ + [Fact(Timeout = 5000)] + public void Pool_should_recycle_up_to_256_stream_states() + { + var ops = new FakeOps(); + var tableSync = new QpackTableSync(0, 4 * 1024, 100, 4 * 1024); + var decoder = new Http3ClientDecoder(tableSync, 16 * 1024); + var mgr = new StreamManager(ops, decoder, tableSync); + + for (var i = 0; i < 256; i++) + { + var streamId = (long)(i * 4); + var state = mgr.GetOrCreateStreamState(streamId); + Assert.NotNull(state); + } + + mgr.DrainStreams(); + + for (var i = 0; i < 256; i++) + { + var streamId = (long)((256 + i) * 4); + var state = mgr.GetOrCreateStreamState(streamId); + Assert.NotNull(state); + } + + mgr.Dispose(); + } +} diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Frames/Http3FrameDecoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Frames/Http3FrameDecoderSpec.cs index 22b5332bb..01efa7f1e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Frames/Http3FrameDecoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Frames/Http3FrameDecoderSpec.cs @@ -176,14 +176,7 @@ public void FrameDecoder_should_handle_byte_at_a_time_feeding() { var status = decoder.TryDecode(new ReadOnlySpan(wire, i, 1), out frame, out _); - if (i < wire.Length - 1) - { - Assert.Equal(DecodeStatus.NeedMoreData, status); - } - else - { - Assert.Equal(DecodeStatus.Success, status); - } + Assert.Equal(i < wire.Length - 1 ? DecodeStatus.NeedMoreData : DecodeStatus.Success, status); } var goaway = Assert.IsType(frame); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderInstructionSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderInstructionSpec.cs index ed390f652..50d837adb 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderInstructionSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderInstructionSpec.cs @@ -78,10 +78,10 @@ public void Should_HaveCorrectPrefixForStreamCancellation() [Trait("RFC", "RFC9204-4.4.2")] public void Should_ThrowForNegativeStreamId_Cancellation() { - Assert.Throws(ThrowHelper_NegativeCancel); + Assert.Throws(ThrowHelperNegativeCancel); return; - static void ThrowHelper_NegativeCancel() + static void ThrowHelperNegativeCancel() { var output = new byte[16]; var writer = SpanWriter.Create(output); @@ -121,9 +121,10 @@ public void Should_HaveCorrectPrefixForInsertCountIncrement() [Trait("RFC", "RFC9204-4.4.3")] public void Should_ThrowForZeroIncrement() { - Assert.Throws(ThrowHelper_ZeroIncrement); + Assert.Throws(ThrowHelperZeroIncrement); + return; - static void ThrowHelper_ZeroIncrement() + static void ThrowHelperZeroIncrement() { var output = new byte[16]; var writer = SpanWriter.Create(output); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderSpec.cs index 7e7800bda..e77353892 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackDecoderSpec.cs @@ -284,8 +284,7 @@ public void Should_DecodeLiteralWithPostBaseNameRef() public void Should_DecodeEmptyHeaderBlock() { var encoder = new QpackEncoder(maxTableCapacity: 0); - var headers = new List<(string, string)>(); - var encoded = encoder.Encode(headers); + var encoded = encoder.Encode(new List<(string, string)>()); var decoder = new QpackDecoder(maxTableCapacity: 0); var decoded = decoder.Decode(encoded.Span); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackRoundTripSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackRoundTripSpec.cs index 5d5e2d7ba..33971718e 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackRoundTripSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Qpack/QpackRoundTripSpec.cs @@ -221,8 +221,7 @@ public void Should_RoundTrip_EmptyHeaderList() var encoder = new QpackEncoder(maxTableCapacity: 4096); var decoder = new QpackDecoder(maxTableCapacity: 4096); - var headers = new List<(string, string)>(); - var encoded = encoder.Encode(headers); + var encoded = encoder.Encode(new List<(string, string)>()); var decoded = decoder.Decode(encoded.Span); Assert.Empty(decoded); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FieldValidationFuzzSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FieldValidationFuzzSpec.cs index 66eea3ba3..b5722703f 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FieldValidationFuzzSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FieldValidationFuzzSpec.cs @@ -26,7 +26,7 @@ public void FieldValidator_should_reject_fully_uppercase_field_name() ("CONTENT-TYPE", "text/html"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -92,7 +92,7 @@ public void FieldValidator_should_reject_crlf_sequence_in_field_value() ("x-inject", "value\r\ninjected-header: evil"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -105,7 +105,7 @@ public void FieldValidator_should_reject_non_token_characters_in_field_name() { var headers = new List<(string Name, string Value)> { (name, "value") }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } } @@ -118,7 +118,7 @@ public void FieldValidator_should_reject_empty_field_name() ("", "value"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -143,7 +143,7 @@ public void FieldValidator_should_reject_transfer_encoding_header() ("transfer-encoding", "chunked"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -155,7 +155,7 @@ public void FieldValidator_should_reject_upgrade_header() ("upgrade", "websocket"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -167,7 +167,7 @@ public void FieldValidator_should_reject_keep_alive_header() ("keep-alive", "timeout=5"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -179,7 +179,7 @@ public void FieldValidator_should_reject_proxy_connection_header() ("proxy-connection", "keep-alive"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -192,7 +192,7 @@ public void FieldValidator_should_reject_te_header_with_non_trailers_value() { var headers = new List<(string Name, string Value)> { ("te", badValue) }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } } @@ -237,7 +237,7 @@ public void FieldValidator_should_reject_unknown_response_pseudo_headers() (pseudo, "value"), }; - var ex = Assert.Throws(() => FieldValidator.ValidateResponsePseudoHeaders(headers)); + Assert.Throws(() => FieldValidator.ValidateResponsePseudoHeaders(headers)); } } @@ -252,7 +252,7 @@ public void FieldValidator_should_reject_pseudo_header_after_regular_header() (":status", "304"), // pseudo after regular — forbidden }; - var ex = Assert.Throws(() => FieldValidator.ValidateResponsePseudoHeaders(headers)); + Assert.Throws(() => FieldValidator.ValidateResponsePseudoHeaders(headers)); } [Fact(Timeout = 5000)] @@ -280,7 +280,7 @@ public void FieldValidator_should_reject_high_ascii_in_field_name() ("caf\u00E9", "value"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FrameFuzzSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FrameFuzzSpec.cs index 5917ac820..1ef378fd9 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FrameFuzzSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3FrameFuzzSpec.cs @@ -115,7 +115,7 @@ public void FrameDecoder_should_handle_single_byte_input_without_crashing() for (byte b = 0; b < 255; b++) { using var decoder = new FrameDecoder(); - var data = new byte[] { b }; + var data = new[] { b }; AssertDecodeNeverCrashes(decoder, data); } @@ -153,7 +153,7 @@ public void FrameDecoder_should_reject_reserved_h2_settings_via_settings_deseria offset += QuicVarInt.Encode(42, payloadBuf.AsSpan(offset)); var payload = payloadBuf[..offset]; - var ex = Assert.Throws(() => Settings.Deserialize(payload)); + Assert.Throws(() => Settings.Deserialize(payload)); } } @@ -172,7 +172,7 @@ public void FrameDecoder_should_reject_duplicate_settings_identifiers() var payload = payloadBuf[..offset]; - var ex = Assert.Throws(() => Settings.Deserialize(payload)); + Assert.Throws(() => Settings.Deserialize(payload)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3SecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3SecuritySpec.cs index 07e8cd743..da15b531b 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3SecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Security/Http3SecuritySpec.cs @@ -116,7 +116,7 @@ public void RejectForbiddenH2Settings_should_throw_for_each_reserved_id() (SettingsIdentifier.ReservedH2EnablePush, 1), }; - var ex = Assert.Throws(() => SettingsIdentifier.RejectForbiddenH2Settings(parameters)); + Assert.Throws(() => SettingsIdentifier.RejectForbiddenH2Settings(parameters)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3FieldValidatorSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3FieldValidatorSpec.cs index 8fbe1501a..1e1a8dc23 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3FieldValidatorSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3FieldValidatorSpec.cs @@ -110,7 +110,7 @@ public void Validate_should_reject_various_uppercase_names(string name) (name, "value"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -224,7 +224,7 @@ public void Validate_should_reject_te_header_with_chunked() ("te", "chunked"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -237,7 +237,7 @@ public void Validate_should_reject_te_header_with_trailers_and_gzip() ("te", "trailers, gzip"), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] @@ -250,7 +250,7 @@ public void Validate_should_reject_te_header_with_empty_value() ("te", ""), }; - var ex = Assert.Throws(() => FieldValidator.Validate(headers)); + Assert.Throws(() => FieldValidator.Validate(headers)); } [Fact(Timeout = 5000)] diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerDecoderSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerDecoderSecuritySpec.cs index 4a4d99a67..b492a448f 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerDecoderSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerDecoderSecuritySpec.cs @@ -4,7 +4,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -[Trait("Component", "Http3ServerDecoder")] public sealed class Http3ServerDecoderSecuritySpec { private readonly QpackTableSync _encoderTableSync = new(encoderMaxCapacity: 0, decoderMaxCapacity: 0); @@ -35,8 +34,6 @@ private static StreamState MakeState(long streamId = 1) return state; } - #region Pseudo-Header Validation Tests - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9114-4.3.1")] public void DecodeHeaders_should_reject_duplicate_method_pseudo_header() @@ -53,7 +50,8 @@ public void DecodeHeaders_should_reject_duplicate_method_pseudo_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":method", ex.Message); Assert.Contains("Duplicate", ex.Message); } @@ -74,7 +72,8 @@ public void DecodeHeaders_should_reject_duplicate_path_pseudo_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":path", ex.Message); Assert.Contains("Duplicate", ex.Message); } @@ -95,7 +94,8 @@ public void DecodeHeaders_should_reject_pseudo_header_after_regular_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("Pseudo-header", ex.Message); Assert.Contains("appears", ex.Message); } @@ -116,14 +116,11 @@ public void DecodeHeaders_should_reject_unknown_pseudo_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":custom", ex.Message); } - #endregion - - #region Forbidden Connection Headers Tests - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9114-4.2")] public void DecodeHeaders_should_reject_connection_header() @@ -140,7 +137,8 @@ public void DecodeHeaders_should_reject_connection_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("connection", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("forbidden", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -161,7 +159,8 @@ public void DecodeHeaders_should_reject_transfer_encoding_header() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("transfer-encoding", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("forbidden", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -182,7 +181,8 @@ public void DecodeHeaders_should_reject_te_with_non_trailers_value() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("te", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("trailers", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -207,10 +207,6 @@ public void DecodeHeaders_should_accept_te_trailers() Assert.NotNull(feature); } - #endregion - - #region CONNECT Edge Cases Tests - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9114-4.4")] public void DecodeHeaders_CONNECT_with_path_should_reject() @@ -225,7 +221,8 @@ public void DecodeHeaders_CONNECT_with_path_should_reject() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":path", ex.Message); } @@ -243,7 +240,8 @@ public void DecodeHeaders_CONNECT_with_scheme_should_reject() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":scheme", ex.Message); } @@ -259,14 +257,11 @@ public void DecodeHeaders_CONNECT_without_authority_should_reject() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + _decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains(":authority", ex.Message); } - #endregion - - #region Field Section Size Tests - [Fact(Timeout = 5000)] [Trait("RFC", "RFC9114-4.2.2")] public void DecodeHeaders_should_reject_field_section_exceeding_max_size() @@ -285,9 +280,8 @@ public void DecodeHeaders_should_reject_field_section_exceeding_max_size() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => decoderWithLimit.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + decoderWithLimit.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("SETTINGS_MAX_FIELD_SECTION_SIZE", ex.Message); } - - #endregion -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerEncoderHardeningSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerEncoderHardeningSpec.cs index 621ee9fbc..bffba4a59 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerEncoderHardeningSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerEncoderHardeningSpec.cs @@ -1,3 +1,5 @@ +using System.Buffers; +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; @@ -5,7 +7,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -[Trait("Component", "Http3ServerEncoderHardening")] public sealed class Http3ServerEncoderHardeningSpec { private readonly QpackTableSync _encoderTableSync = new(encoderMaxCapacity: 4096, decoderMaxCapacity: 4096); @@ -22,8 +23,8 @@ public Http3ServerEncoderHardeningSpec() public void EncodeHeaders_status_should_be_first() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 201); - ctx.Response.Headers["x-test"] = "value"; - ctx.Response.Body = new MemoryStream("test"u8.ToArray()); + ctx.Get()?.Headers["x-test"] = "value"; + ctx.Get()?.Writer.Write("test"u8.ToArray()); var frame = _encoder.EncodeHeaders(ctx); @@ -39,9 +40,9 @@ public void EncodeHeaders_status_should_be_first() public void EncodeHeaders_should_filter_forbidden_headers() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["connection"] = "close"; - ctx.Response.Headers["transfer-encoding"] = "chunked"; - ctx.Response.Headers["x-allowed"] = "yes"; + ctx.Get()?.Headers["connection"] = "close"; + ctx.Get()?.Headers["transfer-encoding"] = "chunked"; + ctx.Get()?.Headers["x-allowed"] = "yes"; var frame = _encoder.EncodeHeaders(ctx); @@ -49,7 +50,7 @@ public void EncodeHeaders_should_filter_forbidden_headers() Assert.DoesNotContain(decoded, h => h.Name == "connection"); Assert.DoesNotContain(decoded, h => h.Name == "transfer-encoding"); - Assert.Contains(decoded, h => h.Name == "x-allowed" && h.Value == "yes"); + Assert.Contains(decoded, h => h is { Name: "x-allowed", Value: "yes" }); } [Fact(Timeout = 5000)] @@ -57,15 +58,15 @@ public void EncodeHeaders_should_filter_forbidden_headers() public void EncodeHeaders_should_lowercase_header_names() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["X-Custom-Header"] = "test-value"; - ctx.Response.Headers["Server"] = "TestServer"; + ctx.Get()?.Headers["X-Custom-Header"] = "test-value"; + ctx.Get()?.Headers["Server"] = "TestServer"; var frame = _encoder.EncodeHeaders(ctx); var decoded = DecodeFrame(frame); - Assert.Contains(decoded, h => h.Name == "x-custom-header" && h.Value == "test-value"); - Assert.Contains(decoded, h => h.Name == "server" && h.Value == "TestServer"); + Assert.Contains(decoded, h => h is { Name: "x-custom-header", Value: "test-value" }); + Assert.Contains(decoded, h => h is { Name: "server", Value: "TestServer" }); Assert.DoesNotContain(decoded, h => h.Name == "X-Custom-Header"); Assert.DoesNotContain(decoded, h => h.Name == "Server"); } @@ -75,16 +76,16 @@ public void EncodeHeaders_should_lowercase_header_names() public void EncodeHeaders_should_include_content_headers() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["content-type"] = "application/json"; - ctx.Response.Headers["content-length"] = "4"; - ctx.Response.Body = new MemoryStream("data"u8.ToArray()); + ctx.Get()?.Headers["content-type"] = "application/json"; + ctx.Get()?.Headers["content-length"] = "4"; + ctx.Get()?.Writer.Write("data"u8.ToArray()); var frame = _encoder.EncodeHeaders(ctx); var decoded = DecodeFrame(frame); Assert.Contains(decoded, h => h.Name == "content-type" && h.Value.Contains("application/json")); - Assert.Contains(decoded, h => h.Name == "content-length" && h.Value == "4"); + Assert.Contains(decoded, h => h is { Name: "content-length", Value: "4" }); } [Fact(Timeout = 5000)] @@ -92,10 +93,10 @@ public void EncodeHeaders_should_include_content_headers() public void EncodeHeaders_multiple_responses_should_not_cross_contaminate() { var ctx1 = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx1.Response.Headers["x-first"] = "first-value"; + ctx1.Get()?.Headers["x-first"] = "first-value"; var ctx2 = ServerTestContext.CreateH3Response(streamId: 3, statusCode: 200); - ctx2.Response.Headers["x-second"] = "second-value"; + ctx2.Get()?.Headers["x-second"] = "second-value"; // Encode response1 with its own encoder/decoder pair var encoder1Sync = new QpackTableSync(encoderMaxCapacity: 4096, decoderMaxCapacity: 4096); @@ -107,6 +108,7 @@ public void EncodeHeaders_multiple_responses_should_not_cross_contaminate() { decoderSync1.ProcessEncoderInstructions(encoder1.EncoderInstructions.Span); } + var decoded1 = decoderSync1.Decoder.Decode(frame1.HeaderBlock.Span, streamId: 1); // Encode response2 with its own encoder/decoder pair @@ -119,6 +121,7 @@ public void EncodeHeaders_multiple_responses_should_not_cross_contaminate() { decoderSync2.ProcessEncoderInstructions(encoder2.EncoderInstructions.Span); } + var decoded2 = decoderSync2.Decoder.Decode(frame2.HeaderBlock.Span, streamId: 3); // Verify each response has its own headers, not the other's @@ -142,4 +145,4 @@ public void EncodeHeaders_multiple_responses_should_not_cross_contaminate() return _decoderTableSync.Decoder.Decode(frame.HeaderBlock.Span, streamId: 1); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineSpec.cs index 18e2fdcee..cda7ae934 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineSpec.cs @@ -1,19 +1,14 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -/// -/// Unit tests for HTTP/3 Http3ServerStateMachine. -/// Tests QUIC stream multiplexing, request assembly from HEADERS/DATA frames, -/// response encoding, and critical stream handling. -/// public sealed class Http3ServerStateMachineSpec { private static byte[] BuildHeadersFrameData(ReadOnlyMemory headerBlock) @@ -130,12 +125,12 @@ public void DecodeClientData_with_headers_should_produce_request_with_stream_id( var context = ops.Requests[0]; // Verify stream ID was stored in request feature - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(streamId, streamIdFeature.StreamId); // Verify request properties - var requestFeature = context.Features.Get() as TurboHttpRequestFeature; + var requestFeature = context.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature); Assert.Equal("GET", requestFeature.Method); Assert.Equal("https", requestFeature.Scheme); @@ -181,12 +176,12 @@ public async Task DecodeClientData_with_headers_and_data_should_accumulate_body( var context = ops.Requests[0]; // Verify stream ID - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(streamId, streamIdFeature.StreamId); // Verify request properties - var requestFeature = context.Features.Get() as TurboHttpRequestFeature; + var requestFeature = context.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature); Assert.Equal("POST", requestFeature.Method); Assert.Equal("https", requestFeature.Scheme); @@ -194,9 +189,7 @@ public async Task DecodeClientData_with_headers_and_data_should_accumulate_body( Assert.Equal("/api/data", requestFeature.Path); // Verify body was accumulated - var bodyFeature = context.Features.Get(); - Assert.NotNull(bodyFeature); - var bodyStream = bodyFeature.Body; + var bodyStream = requestFeature.Body; var content = await new StreamReader(bodyStream).ReadToEndAsync(TestContext.Current.CancellationToken); Assert.Equal(bodyContent, content); } @@ -227,7 +220,7 @@ public void OnResponse_no_body_should_emit_HEADERS_and_CompleteWrites() var context = ops.Requests[0]; // Verify StreamIdKey is set - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(streamId, streamIdFeature.StreamId); @@ -235,8 +228,7 @@ public void OnResponse_no_body_should_emit_HEADERS_and_CompleteWrites() ops.Outbound.Clear(); // Send response without body - context.Response.StatusCode = 200; - context.Response.ContentLength = 0; + context.Get()?.StatusCode = 200; sm.OnResponse(context); // Should emit HEADERS frame + CompleteWrites immediately (no body) @@ -279,7 +271,8 @@ public void OnResponse_with_body_should_schedule_drain_timer() ops.Outbound.Clear(); // Send response with body - context.Response.StatusCode = 200; + context.Get()?.StatusCode = 200; + context.Get()?.Headers["Content-Length"] = "100"; sm.OnResponse(context); // Should emit HEADERS frame immediately @@ -332,16 +325,16 @@ public void DecodeClientData_with_multiple_streams_should_multiplex() var ctx2 = ops.Requests[1]; // Verify stream IDs - var streamIdFeature1 = ctx1.Features.Get(); + var streamIdFeature1 = ctx1.Get(); Assert.NotNull(streamIdFeature1); - var streamIdFeature2 = ctx2.Features.Get(); + var streamIdFeature2 = ctx2.Get(); Assert.NotNull(streamIdFeature2); Assert.Equal(stream1, streamIdFeature1.StreamId); Assert.Equal(stream2, streamIdFeature2.StreamId); // Verify different requests - var requestFeature1 = ctx1.Features.Get() as TurboHttpRequestFeature; - var requestFeature2 = ctx2.Features.Get() as TurboHttpRequestFeature; + var requestFeature1 = ctx1.Get() as TurboHttpRequestFeature; + var requestFeature2 = ctx2.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature1); Assert.NotNull(requestFeature2); Assert.Equal("GET", requestFeature1.Method); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineTimerSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineTimerSpec.cs index d1fb96063..3c2ba7aa8 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineTimerSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStateMachineTimerSpec.cs @@ -1,22 +1,16 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -/// -/// Unit tests for HTTP/3 Http3ServerStateMachine timer behavior and error recovery. -/// Tests keep-alive timeout, headers-timeout RST emission, cleanup idempotency, -/// and proper request flushing on downstream finish. -/// public sealed class Http3ServerStateMachineTimerSpec { - private static void SendRequest(Http3ServerStateMachine sm, long streamId) { var ts = new QpackTableSync(0, 0, 0, 0); @@ -190,13 +184,11 @@ public void OnDownstreamFinished_should_flush_pending() // Request should now be emitted Assert.Single(ops.Requests); var context = ops.Requests[0]; - var requestFeature = context.Features.Get() as TurboHttpRequestFeature; + var requestFeature = context.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature); Assert.Equal("GET", requestFeature.Method); Assert.Equal("https", requestFeature.Scheme); Assert.Equal("localhost", requestFeature.ExtractedHost); Assert.Equal("/", requestFeature.Path); } -} - - +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStreamResolverSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStreamResolverSpec.cs index 46eb504ef..10a4e05e4 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStreamResolverSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Http3ServerStreamResolverSpec.cs @@ -12,9 +12,9 @@ public void Resolve_should_detect_control_stream() { var resolver = new ServerStreamResolver(); var buffer = BuildStreamTypeBuffer(StreamType.Control); - resolver.OnServerStreamOpened(1); + resolver.OnServerStreamOpened(3); - var result = resolver.Resolve(1, buffer); + var result = resolver.Resolve(3, buffer); Assert.Equal(CriticalStreamId.ControlId, result.LogicalStreamId); Assert.Null(result.Buffer); @@ -26,9 +26,9 @@ public void Resolve_should_detect_qpack_encoder_stream() { var resolver = new ServerStreamResolver(); var buffer = BuildStreamTypeBuffer(StreamType.QpackEncoder); - resolver.OnServerStreamOpened(3); + resolver.OnServerStreamOpened(7); - var result = resolver.Resolve(3, buffer); + var result = resolver.Resolve(7, buffer); Assert.Equal(CriticalStreamId.QpackEncoderId, result.LogicalStreamId); Assert.Null(result.Buffer); @@ -40,9 +40,9 @@ public void Resolve_should_detect_qpack_decoder_stream() { var resolver = new ServerStreamResolver(); var buffer = BuildStreamTypeBuffer(StreamType.QpackDecoder); - resolver.OnServerStreamOpened(5); + resolver.OnServerStreamOpened(11); - var result = resolver.Resolve(5, buffer); + var result = resolver.Resolve(11, buffer); Assert.Equal(CriticalStreamId.QpackDecoderId, result.LogicalStreamId); Assert.Null(result.Buffer); @@ -55,12 +55,12 @@ public void Resolve_should_reject_duplicate_control_stream() var resolver = new ServerStreamResolver(); var buffer1 = BuildStreamTypeBuffer(StreamType.Control); var buffer2 = BuildStreamTypeBuffer(StreamType.Control); - resolver.OnServerStreamOpened(1); resolver.OnServerStreamOpened(3); + resolver.OnServerStreamOpened(7); - resolver.Resolve(1, buffer1); + resolver.Resolve(3, buffer1); - var ex = Assert.Throws(() => resolver.Resolve(3, buffer2)); + var ex = Assert.Throws(() => resolver.Resolve(7, buffer2)); Assert.Contains("Duplicate stream type", ex.Message); Assert.Contains("Control", ex.Message); } @@ -72,12 +72,12 @@ public void Resolve_should_reject_duplicate_qpack_encoder_stream() var resolver = new ServerStreamResolver(); var buffer1 = BuildStreamTypeBuffer(StreamType.QpackEncoder); var buffer2 = BuildStreamTypeBuffer(StreamType.QpackEncoder); - resolver.OnServerStreamOpened(1); resolver.OnServerStreamOpened(3); + resolver.OnServerStreamOpened(7); - resolver.Resolve(1, buffer1); + resolver.Resolve(3, buffer1); - var ex = Assert.Throws(() => resolver.Resolve(3, buffer2)); + var ex = Assert.Throws(() => resolver.Resolve(7, buffer2)); Assert.Contains("Duplicate stream type", ex.Message); Assert.Contains("QpackEncoder", ex.Message); } @@ -89,12 +89,12 @@ public void Resolve_should_reject_duplicate_qpack_decoder_stream() var resolver = new ServerStreamResolver(); var buffer1 = BuildStreamTypeBuffer(StreamType.QpackDecoder); var buffer2 = BuildStreamTypeBuffer(StreamType.QpackDecoder); - resolver.OnServerStreamOpened(1); resolver.OnServerStreamOpened(3); + resolver.OnServerStreamOpened(7); - resolver.Resolve(1, buffer1); + resolver.Resolve(3, buffer1); - var ex = Assert.Throws(() => resolver.Resolve(3, buffer2)); + var ex = Assert.Throws(() => resolver.Resolve(7, buffer2)); Assert.Contains("Duplicate stream type", ex.Message); Assert.Contains("QpackDecoder", ex.Message); } @@ -106,9 +106,9 @@ public void Resolve_should_trim_stream_type_and_preserve_remaining_data() var resolver = new ServerStreamResolver(); var extraData = new byte[] { 0xAA, 0xBB, 0xCC }; var buffer = BuildStreamTypeBuffer(StreamType.Control, extraData); - resolver.OnServerStreamOpened(1); + resolver.OnServerStreamOpened(3); - var result = resolver.Resolve(1, buffer); + var result = resolver.Resolve(3, buffer); Assert.Equal(CriticalStreamId.ControlId, result.LogicalStreamId); Assert.NotNull(result.Buffer); @@ -123,9 +123,9 @@ public void Resolve_should_return_null_buffer_when_no_remaining_data() { var resolver = new ServerStreamResolver(); var buffer = BuildStreamTypeBuffer(StreamType.Control); - resolver.OnServerStreamOpened(1); + resolver.OnServerStreamOpened(3); - var result = resolver.Resolve(1, buffer); + var result = resolver.Resolve(3, buffer); Assert.Equal(CriticalStreamId.ControlId, result.LogicalStreamId); Assert.Null(result.Buffer); @@ -154,18 +154,33 @@ public void Reset_should_clear_all_state() var resolver = new ServerStreamResolver(); var buffer1 = BuildStreamTypeBuffer(StreamType.Control); var buffer2 = BuildStreamTypeBuffer(StreamType.Control); - resolver.OnServerStreamOpened(1); + resolver.OnServerStreamOpened(3); - resolver.Resolve(1, buffer1); + resolver.Resolve(3, buffer1); resolver.Reset(); - resolver.OnServerStreamOpened(3); + resolver.OnServerStreamOpened(7); - var result = resolver.Resolve(3, buffer2); + var result = resolver.Resolve(7, buffer2); Assert.Equal(CriticalStreamId.ControlId, result.LogicalStreamId); Assert.Null(result.Buffer); } + [Fact(Timeout = 5000)] + [Trait("RFC", "RFC9000-2.1")] + public void OnServerStreamOpened_should_ignore_bidirectional_streams() + { + var resolver = new ServerStreamResolver(); + var buffer = BuildStreamTypeBuffer(StreamType.Control); + resolver.OnServerStreamOpened(0); + resolver.OnServerStreamOpened(1); + resolver.OnServerStreamOpened(4); + resolver.OnServerStreamOpened(5); + + var result = resolver.Resolve(0, buffer); + Assert.Equal(0L, result.LogicalStreamId); + } + private static TransportBuffer BuildStreamTypeBuffer(StreamType streamType, byte[]? extraData = null) { var typeBytes = new byte[8]; @@ -173,10 +188,7 @@ private static TransportBuffer BuildStreamTypeBuffer(StreamType streamType, byte var totalSize = typeLen + (extraData?.Length ?? 0); var buffer = TransportBuffer.Rent(totalSize); typeBytes.AsSpan(0, typeLen).CopyTo(buffer.FullMemory.Span); - if (extraData != null) - { - extraData.CopyTo(buffer.FullMemory.Span[typeLen..]); - } + extraData?.CopyTo(buffer.FullMemory.Span[typeLen..]); buffer.Length = totalSize; return buffer; diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Security/Http3ServerSecuritySpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Security/Http3ServerSecuritySpec.cs index c35c5131d..25b3e3c20 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Security/Http3ServerSecuritySpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/Security/Http3ServerSecuritySpec.cs @@ -4,7 +4,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server.Security; -[Trait("Component", "Http3ServerDecoder")] public sealed class Http3ServerSecuritySpec { private readonly QpackTableSync _encoderSync = new(0, 0, 0, 0); @@ -47,7 +46,8 @@ public void Field_section_exceeding_max_size_should_be_rejected() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("SETTINGS_MAX_FIELD_SECTION_SIZE", ex.Message); } @@ -73,7 +73,8 @@ public void Many_small_headers_exceeding_total_field_section_size_should_be_reje var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("SETTINGS_MAX_FIELD_SECTION_SIZE", ex.Message); } @@ -95,7 +96,8 @@ public void Uppercase_header_name_should_be_rejected() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("uppercase", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -117,7 +119,8 @@ public void Header_value_with_null_byte_should_be_rejected() var frame = EncodeAndSync(headers); var state = MakeState(); - var ex = Assert.Throws(() => decoder.DecodeHeadersToFeature(frame, state, endStream: true)); + var ex = Assert.Throws(() => + decoder.DecodeHeadersToFeature(frame, state, endStream: true)); Assert.Contains("NUL", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -125,8 +128,6 @@ public void Header_value_with_null_byte_should_be_rejected() [Trait("RFC", "RFC9114-10.3")] public void Empty_header_name_should_be_rejected() { - var decoder = new Http3ServerDecoder(_decoderSync); - var headers = new List<(string Name, string Value)> { (":method", "GET"), diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerRequestDecoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerRequestDecoderSpec.cs index cea36f992..53b6c43e7 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerRequestDecoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerRequestDecoderSpec.cs @@ -4,7 +4,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -[Trait("Component", "Http3ServerRequestDecoder")] public sealed class ServerRequestDecoderSpec { private readonly QpackTableSync _encoderTableSync = new(encoderMaxCapacity: 4096, decoderMaxCapacity: 4096); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerResponseEncoderSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerResponseEncoderSpec.cs index ec69c1fe1..825f99081 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerResponseEncoderSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/ServerResponseEncoderSpec.cs @@ -1,3 +1,5 @@ +using System.Buffers; +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; @@ -5,7 +7,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server; -[Trait("Component", "Http3ServerResponseEncoder")] public sealed class ServerResponseEncoderSpec { private readonly QpackTableSync _encoderTableSync = new(encoderMaxCapacity: 4096, decoderMaxCapacity: 4096); @@ -40,7 +41,7 @@ public void EncodeHeaders_200_OK_returns_single_HEADERS_frame() public void EncodeHeaders_200_with_body_returns_HEADERS_frame_only() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Body = new MemoryStream("test response body"u8.ToArray()); + ctx.Get()?.Writer.Write("test response body"u8.ToArray()); var frame = _encoder.EncodeHeaders(ctx); @@ -59,8 +60,8 @@ public void EncodeHeaders_200_with_body_returns_HEADERS_frame_only() public void EncodeHeaders_status_is_first_header() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 201); - ctx.Response.Headers["custom-header"] = "value"; - ctx.Response.Body = new MemoryStream("test"u8.ToArray()); + ctx.Get()?.Headers["custom-header"] = "value"; + ctx.Get()?.Writer.Write("test"u8.ToArray()); var headersFrame = _encoder.EncodeHeaders(ctx); @@ -83,9 +84,9 @@ public void EncodeHeaders_status_is_first_header() public void EncodeHeaders_forbidden_headers_are_filtered() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["connection"] = "close"; - ctx.Response.Headers["transfer-encoding"] = "chunked"; - ctx.Response.Headers["custom-allowed"] = "yes"; + ctx.Get()?.Headers["connection"] = "close"; + ctx.Get()?.Headers["transfer-encoding"] = "chunked"; + ctx.Get()?.Headers["custom-allowed"] = "yes"; var headersFrame = _encoder.EncodeHeaders(ctx); @@ -109,8 +110,8 @@ public void EncodeHeaders_forbidden_headers_are_filtered() public void EncodeHeaders_header_names_are_lowercase() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["X-Custom-Header"] = "value"; - ctx.Response.Headers["Server"] = "TestServer"; + ctx.Get()?.Headers["X-Custom-Header"] = "value"; + ctx.Get()?.Headers["Server"] = "TestServer"; var headersFrame = _encoder.EncodeHeaders(ctx); @@ -135,9 +136,9 @@ public void EncodeHeaders_header_names_are_lowercase() public void EncodeHeaders_content_headers_are_included() { var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Headers["content-type"] = "application/json"; - ctx.Response.Headers["content-length"] = "4"; - ctx.Response.Body = new MemoryStream("data"u8.ToArray()); + ctx.Get()?.Headers["content-type"] = "application/json"; + ctx.Get()?.Headers["content-length"] = "4"; + ctx.Get()?.Writer.Write("data"u8.ToArray()); var headersFrame = _encoder.EncodeHeaders(ctx); @@ -163,7 +164,7 @@ public void EncodeHeaders_with_large_body_returns_HEADERS_frame_only() Array.Fill(largeData, (byte)'x'); var ctx = ServerTestContext.CreateH3Response(streamId: 1, statusCode: 200); - ctx.Response.Body = new MemoryStream(largeData); + ctx.Get()?.Writer.Write(largeData); var frame = _encoder.EncodeHeaders(ctx); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3BodyRateTimeoutSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3BodyRateTimeoutSpec.cs index b2a8dba4a..70b6c7f23 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3BodyRateTimeoutSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3BodyRateTimeoutSpec.cs @@ -1,23 +1,17 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Options; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server.SessionManager; -/// -/// Unit tests for HTTP/3 Http3ServerSessionManager body rate checking and timeout handling. -/// Tests that DATA frames trigger body-rate-check timers, and that headers-timeout is properly -/// cancelled upon successful decoding or stream completion. -/// public sealed class Http3BodyRateTimeoutSpec { - - private static (byte[] Data, long StreamId) BuildRequest(string method, string path, long streamId) + private static byte[] BuildRequest(string method, string path) { var tableSync = new QpackTableSync(0, 0, 0, 0); var headers = new List<(string, string)> @@ -32,7 +26,7 @@ private static (byte[] Data, long StreamId) BuildRequest(string method, string p var buf = new byte[frame.SerializedSize]; var span = buf.AsSpan(); frame.WriteTo(ref span); - return (buf, streamId); + return buf; } private static byte[] BuildDataFrameBytes(int size) @@ -62,7 +56,7 @@ public void First_DATA_frame_should_schedule_body_rate_check() const long streamId = 4; // Build HEADERS - var (headerBytes, _) = BuildRequest("POST", "/upload", streamId); + var headerBytes = BuildRequest("POST", "/upload"); // Open stream sm.DecodeClientData(new ServerStreamAccepted(StreamTarget.FromId(streamId), @@ -102,7 +96,7 @@ public void Headers_timeout_should_be_cancelled_on_successful_decode() const long streamId = 8; // Build HEADERS - var (headerBytes, _) = BuildRequest("GET", "/", streamId); + var headerBytes = BuildRequest("GET", "/"); // Open stream sm.DecodeClientData(new ServerStreamAccepted(StreamTarget.FromId(streamId), @@ -138,7 +132,7 @@ public void StreamReadCompleted_without_body_should_emit_request_with_empty_cont const long streamId = 12; // Build HEADERS - var (headerBytes, _) = BuildRequest("GET", "/", streamId); + var headerBytes = BuildRequest("GET", "/"); // Open stream sm.DecodeClientData(new ServerStreamAccepted(StreamTarget.FromId(streamId), @@ -157,7 +151,7 @@ public void StreamReadCompleted_without_body_should_emit_request_with_empty_cont Assert.Single(ops.Requests); var context = ops.Requests[0]; - var requestFeature = context.Features.Get() as TurboHttpRequestFeature; + var requestFeature = context.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature); Assert.Equal("GET", requestFeature.Method); Assert.Equal("https", requestFeature.Scheme); diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3CriticalStreamsSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3CriticalStreamsSpec.cs index 80535dfa7..eae39c1dd 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3CriticalStreamsSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3CriticalStreamsSpec.cs @@ -6,11 +6,6 @@ namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server.SessionManager; -/// -/// Unit tests for HTTP/3 Http3ServerSessionManager critical streams and SETTINGS frame. -/// Tests that PreStart() opens control, qpack encoder, and qpack decoder streams, -/// and emits SETTINGS frame on the control stream per RFC 9114. -/// public sealed class Http3CriticalStreamsSpec { diff --git a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3StreamLifecycleSpec.cs b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3StreamLifecycleSpec.cs index 22e7a72b1..bccfcb689 100644 --- a/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3StreamLifecycleSpec.cs +++ b/src/TurboHTTP.Tests/Protocol/Syntax/Http3/Server/SessionManager/Http3StreamLifecycleSpec.cs @@ -1,22 +1,17 @@ using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Syntax.Http3; using TurboHTTP.Protocol.Syntax.Http3.Options; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Protocol.Syntax.Http3.Server; -using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Tests.Shared; namespace TurboHTTP.Tests.Protocol.Syntax.Http3.Server.SessionManager; -/// -/// Unit tests for HTTP/3 Http3ServerSessionManager stream lifecycle. -/// Tests request emission, concurrent streams, response handling, and cleanup. -/// public sealed class Http3StreamLifecycleSpec { - private static TurboHttpContext CreateResponseContext(long streamId = 999) + private static IFeatureCollection CreateResponseContext(long streamId = 999) { var features = new TurboFeatureCollection(); features.Set(new TurboHttpRequestFeature()); @@ -24,12 +19,12 @@ private static TurboHttpContext CreateResponseContext(long streamId = 999) features.Set(new TurboStreamIdFeature(streamId)); var bodyFeature = new TurboHttpResponseBodyFeature(); features.Set(bodyFeature); - features.Set(bodyFeature); - return new TurboHttpContext(features); + features.Set(bodyFeature); + return features; } - private static (byte[] Data, long StreamId) BuildRequest(string method, string path, long streamId) + private static byte[] BuildRequest(string method, string path) { var tableSync = new QpackTableSync(0, 0, 0, 0); var headers = new List<(string, string)> @@ -44,13 +39,13 @@ private static (byte[] Data, long StreamId) BuildRequest(string method, string p var buf = new byte[frame.SerializedSize]; var span = buf.AsSpan(); frame.WriteTo(ref span); - return (buf, streamId); + return buf; } private static void SendRequest(Http3ServerSessionManager sm, long streamId, string method = "GET", string path = "/") { - var (data, _) = BuildRequest(method, path, streamId); + var data = BuildRequest(method, path); sm.DecodeClientData(new ServerStreamAccepted(StreamTarget.FromId(streamId), StreamDirection.Bidirectional)); var buffer = TransportBuffer.Rent(data.Length); @@ -80,10 +75,10 @@ public void Request_should_be_emitted_after_StreamReadCompleted() Assert.Single(ops.Requests); var context = ops.Requests[0]; - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(streamId, streamIdFeature.StreamId); - var requestFeature = context.Features.Get() as TurboHttpRequestFeature; + var requestFeature = context.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature); Assert.Equal("GET", requestFeature.Method); Assert.Equal("https", requestFeature.Scheme); @@ -109,15 +104,15 @@ public void Multiple_concurrent_streams_should_all_emit_requests() var ctx1 = ops.Requests[0]; var ctx2 = ops.Requests[1]; - var streamIdFeature1 = ctx1.Features.Get(); + var streamIdFeature1 = ctx1.Get(); Assert.NotNull(streamIdFeature1); - var streamIdFeature2 = ctx2.Features.Get(); + var streamIdFeature2 = ctx2.Get(); Assert.NotNull(streamIdFeature2); Assert.Equal(streamId1, streamIdFeature1.StreamId); Assert.Equal(streamId2, streamIdFeature2.StreamId); - var requestFeature1 = ctx1.Features.Get() as TurboHttpRequestFeature; - var requestFeature2 = ctx2.Features.Get() as TurboHttpRequestFeature; + var requestFeature1 = ctx1.Get() as TurboHttpRequestFeature; + var requestFeature2 = ctx2.Get() as TurboHttpRequestFeature; Assert.NotNull(requestFeature1); Assert.NotNull(requestFeature2); Assert.Equal("GET", requestFeature1.Method); @@ -155,8 +150,7 @@ public void OnResponse_no_body_should_emit_CompleteWrites() ops.Outbound.Clear(); - context.Response.StatusCode = 200; - context.Response.ContentLength = 0; + context.Get()?.StatusCode = 200; sm.OnResponse(context); var completeWrites = ops.Outbound.OfType().ToList(); @@ -190,7 +184,7 @@ public void FlushAllPendingRequests_should_emit_pending() var sm = CreateSM(ops); const long streamId = 16; - var (data, _) = BuildRequest("GET", "/", streamId); + var data = BuildRequest("GET", "/"); sm.DecodeClientData(new ServerStreamAccepted(StreamTarget.FromId(streamId), StreamDirection.Bidirectional)); @@ -210,8 +204,8 @@ public void FlushAllPendingRequests_should_emit_pending() Assert.Single(ops.Requests); var context = ops.Requests[0]; - var streamIdFeature = context.Features.Get(); + var streamIdFeature = context.Get(); Assert.NotNull(streamIdFeature); Assert.Equal(streamId, streamIdFeature.StreamId); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Routing/Binding/AsParametersBinderSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/AsParametersBinderSpec.cs deleted file mode 100644 index d5f8c6b76..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/AsParametersBinderSpec.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class AsParametersBinderSpec -{ - [Fact(Timeout = 5000)] - public async Task AsParameters_should_bind_flat_dto_from_route_and_query() - { - Delegate handler = ([AsParameters] ItemQuery q) => TypedResults.Ok(string.Concat(q.Id, "-", q.Page)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/42?page=3"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task AsParameters_should_bind_with_FromHeader_on_property() - { - Delegate handler = ([AsParameters] TenantQuery q) - => TypedResults.Ok(string.Concat(q.Id, "-", q.Tenant)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/42"); - ctx.Request.RouteValues["id"] = "42"; - ctx.Request.Headers["X-Tenant"] = "acme"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task AsParameters_should_bind_nested_complex_type() - { - Delegate handler = ([AsParameters] OuterQuery q) - => TypedResults.Ok(string.Concat(q.Id, "-", q.Paging.Page, "-", q.Paging.Size)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/42?page=2&size=10"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void AsParameters_should_detect_circular_reference() - { - Delegate handler = ([AsParameters] CircularA q) => TypedResults.Ok(); - Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - } - - [Fact(Timeout = 5000)] - public async Task AsParameters_should_validate_annotated_properties() - { - Delegate handler = ([AsParameters] ValidatedQuery q) => TypedResults.Ok(q.Name); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/42"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(400, ctx.Response.StatusCode); - } - - private sealed record ItemQuery( - [property: FromRoute] string Id, - [property: FromQuery] int Page); - - private sealed record TenantQuery( - [property: FromRoute] string Id, - [property: FromHeader(Name = "X-Tenant")] string Tenant); - - private sealed record OuterQuery( - [property: FromRoute] string Id, - [AsParameters] PagingQuery Paging); - - private sealed record PagingQuery( - [property: FromQuery] int Page, - [property: FromQuery] int Size); - - private sealed record ValidatedQuery( - [property: FromRoute] string Id, - [property: Required] string Name); - - public sealed record CircularA([AsParameters] CircularB B); - - public sealed record CircularB([AsParameters] CircularA A); - - private static IServiceProvider CreateServiceProvider() - => new ServiceCollection().AddLogging().BuildServiceProvider(); - - private static TurboHttpContext CreateContext(string path) - { - return ServerTestContext.Request() - .Get(path) - .Services(CreateServiceProvider()) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/AttributeBindingSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/AttributeBindingSpec.cs deleted file mode 100644 index 5ff944359..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/AttributeBindingSpec.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class AttributeBindingSpec -{ - [Fact(Timeout = 5000)] - public async Task FromRoute_should_override_convention() - { - Delegate handler = ([FromRoute] string id) => TypedResults.Ok(string.Concat("route-", id)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/42"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromQuery_should_bind_from_query_string() - { - Delegate handler = ([FromQuery] string q) => TypedResults.Ok(string.Concat("search-", q)); - var bound = DelegateHandlerBinder.Bind("/search", handler); - var ctx = CreateContext("/search?q=hello"); - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromQuery_should_override_route_convention() - { - Delegate handler = ([FromQuery] string id) => TypedResults.Ok(string.Concat("query-", id)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/ignored?id=from-query"); - ctx.Request.RouteValues["id"] = "ignored"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromHeader_should_bind_from_request_header() - { - Delegate handler = ([FromHeader(Name = "X-Tenant")] string tenant) - => TypedResults.Ok(string.Concat("tenant-", tenant)); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - ctx.Request.Headers["X-Tenant"] = "acme"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromHeader_should_use_parameter_name_when_Name_not_set() - { - Delegate handler = ([FromHeader] string accept) => TypedResults.Ok(string.Concat("accept-", accept)); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - ctx.Request.Headers["accept"] = "application/json"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromRoute_with_custom_name_should_bind_from_named_segment() - { - Delegate handler = ([FromRoute(Name = "userId")] string id) => TypedResults.Ok(string.Concat("user-", id)); - var bound = DelegateHandlerBinder.Bind("/users/{userId}", handler); - var ctx = CreateContext("/users/99"); - ctx.Request.RouteValues["userId"] = "99"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromServices_should_resolve_from_di() - { - Delegate handler = ([FromServices] IServiceProvider sp) => TypedResults.Ok("ok"); - var services = new ServiceCollection().AddLogging().BuildServiceProvider(); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - await bound(ctx, services); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task FromBody_should_deserialize_json() - { - Delegate handler = ([FromBody] CreateItemDto body) => TypedResults.Ok(body.Name); - var bound = DelegateHandlerBinder.Bind("/items", handler); - var ctx = CreateContextWithJsonBody("/items", """{"Name":"Widget","Quantity":5}"""); - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Convention_should_still_work_without_attributes() - { - Delegate handler = (string id) => TypedResults.Ok(string.Concat("conv-", id)); - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/7"); - ctx.Request.RouteValues["id"] = "7"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - private sealed record CreateItemDto(string Name, int Quantity); - - private sealed record ValidatedDto( - [property: System.ComponentModel.DataAnnotations.Required] string Name, - [property: System.ComponentModel.DataAnnotations.Range(1, 100)] - int Quantity); - - private static IServiceProvider CreateServiceProvider() - { - return new ServiceCollection().AddLogging().BuildServiceProvider(); - } - - private static TurboHttpContext CreateContext(string path) - { - return ServerTestContext.Request() - .Get(path) - .Services(CreateServiceProvider()) - .Build(); - } - - private static TurboHttpContext CreateContextWithJsonBody(string path, string json) - { - return ServerTestContext.Request() - .Post(path) - .JsonBody(json) - .Services(CreateServiceProvider()) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/DelegateHandlerBinderSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/DelegateHandlerBinderSpec.cs deleted file mode 100644 index 9d40f41b7..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/DelegateHandlerBinderSpec.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class DelegateHandlerBinderSpec : StreamTestBase -{ - [Fact(Timeout = 5000)] - public async Task Bind_should_handle_no_params_IResult_return() - { - Delegate handler = () => TypedResults.Ok("hello"); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_inject_route_value() - { - Delegate handler = (string id) => TypedResults.Ok(string.Concat("order-", id)); - var bound = DelegateHandlerBinder.Bind("/orders/{id}", handler); - var ctx = CreateContext("/orders/42"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_inject_route_value_as_int() - { - Delegate handler = (int id) => TypedResults.Ok(string.Concat("order-", id)); - var bound = DelegateHandlerBinder.Bind("/orders/{id}", handler); - var ctx = CreateContext("/orders/42"); - ctx.Request.RouteValues["id"] = "42"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_inject_di_service() - { - Delegate handler = (ITestService svc) => TypedResults.Ok(svc.GetValue()); - var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(new TestService("injected")); - var provider = services.BuildServiceProvider(); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - await bound(ctx, provider); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_inject_turbo_context() - { - Delegate handler = (TurboHttpContext ctx) => TypedResults.Ok(ctx.Request.Method); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_handle_async_handler() - { - Delegate handler = async (string id) => - { - await Task.Delay(1); - return TypedResults.Ok(string.Concat("async-", id)); - }; - var bound = DelegateHandlerBinder.Bind("/items/{id}", handler); - var ctx = CreateContext("/items/7"); - ctx.Request.RouteValues["id"] = "7"; - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Bind_should_inject_http_context_base_type() - { - Delegate handler = (HttpContext ctx) => TypedResults.Ok(ctx.Request.Method); - var bound = DelegateHandlerBinder.Bind("/test", handler); - var ctx = CreateContext("/test"); - await bound(ctx, CreateServiceProvider()); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void Bind_should_reject_non_IResult_return() - { - Delegate handler = () => "not IResult"; - Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - } - - [Fact(Timeout = 5000)] - public void Bind_should_reject_HttpResponseMessage_return() - { - Delegate handler = () => new HttpResponseMessage(HttpStatusCode.Accepted); - Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - } - - [Fact(Timeout = 5000)] - public void Bind_should_reject_void_return() - { - Delegate handler = () => { }; - Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - } - - private interface ITestService - { - string GetValue(); - } - - private sealed class TestService : ITestService - { - private readonly string _value; - - public TestService(string value) - { - _value = value; - } - - public string GetValue() => _value; - } - - private static IServiceProvider CreateServiceProvider() - { - return new ServiceCollection() - .AddLogging() - .BuildServiceProvider(); - } - - private TurboHttpContext CreateContext(string path) - { - return ServerTestContext.Request() - .Get(path) - .Connection(new TurboConnectionInfo("test", null, 0, null, 0)) - .Services(new ServiceCollection().AddLogging().BuildServiceProvider()) - .Materializer(Materializer) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/DelegateRoutingIntegrationSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/DelegateRoutingIntegrationSpec.cs deleted file mode 100644 index 01fe8dbef..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/DelegateRoutingIntegrationSpec.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; -using TurboHTTP.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class DelegateRoutingIntegrationSpec -{ - [Fact(Timeout = 5000)] - public void MapTurboGet_with_delegate_should_register_route() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - app.MapTurboGet("/health", () => TypedResults.Ok("ok")); - - var table = app.Services.GetRequiredService(); - var result = table.Freeze().Match("GET", "/health"); - Assert.True(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public async Task MapTurboGet_with_delegate_should_invoke_handler() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - app.MapTurboGet("/health", () => TypedResults.Ok("healthy")); - - var table = app.Services.GetRequiredService(); - var result = table.Freeze().Match("GET", "/health"); - Assert.True(result.IsMatch); - - var ctx = CreateContext("/health"); - ctx.RequestServices = app.Services; - await result.Dispatcher!.DispatchAsync(ctx, CancellationToken.None); - - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void MapTurboGroup_with_delegate_should_register_prefixed_route() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var api = app.MapTurboGroup("/api"); - api.MapGet("/users", () => TypedResults.Ok("users")); - - var table = app.Services.GetRequiredService(); - Assert.True(table.Freeze().Match("GET", "/api/users").IsMatch); - } - - private static TurboHttpContext CreateContext(string path) - { - return ServerTestContext.Request() - .Get(path) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/FormBindingSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/FormBindingSpec.cs deleted file mode 100644 index 9d059636c..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/FormBindingSpec.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class FormBindingSpec -{ - [Fact(Timeout = 5000)] - public async Task FormBinder_should_extract_urlencoded_value() - { - var binder = new FormBinder("name", typeof(string)); - var ctx = CreateUrlEncodedContext("/submit", "name=Alice&age=30"); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal("Alice", result); - } - - [Fact(Timeout = 5000)] - public async Task FormBinder_should_parse_int_from_urlencoded() - { - var binder = new FormBinder("age", typeof(int)); - var ctx = CreateUrlEncodedContext("/submit", "name=Alice&age=30"); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(30, result); - } - - [Fact(Timeout = 5000)] - public async Task FormBinder_should_return_null_when_key_missing() - { - var binder = new FormBinder("missing", typeof(string)); - var ctx = CreateUrlEncodedContext("/submit", "name=Alice"); - var result = await binder.BindAsync(ctx, null!); - Assert.Null(result); - } - - [Fact(Timeout = 5000)] - public async Task FormFileBinder_should_extract_file_from_multipart() - { - var binder = new FormFileBinder("file"); - var ctx = CreateMultipartContext("/upload", "file", "test.txt", "hello world"u8.ToArray()); - var result = await binder.BindAsync(ctx, null!); - var file = Assert.IsAssignableFrom(result); - Assert.Equal("test.txt", file.FileName); - Assert.Equal(11, file.Length); - } - - [Fact(Timeout = 5000)] - public async Task FromForm_attribute_should_bind_in_handler() - { - var captured = ""; - Delegate handler = ([FromForm] string name, [FromForm] int age) => - { - captured = string.Concat(name, "-", age); - return TypedResults.Ok("success"); - }; - var bound = DelegateHandlerBinder.Bind("/submit", handler); - var ctx = CreateUrlEncodedContext("/submit", "name=Alice&age=30"); - var services = CreateServiceProvider(); - ctx.RequestServices = services; - await bound(ctx, services); - Assert.Equal(200, ctx.Response.StatusCode); - Assert.Equal("Alice-30", captured); - } - - [Fact(Timeout = 5000)] - public void FromBody_and_FromForm_should_be_mutually_exclusive() - { - Delegate handler = ([FromBody] string a, [FromForm] string b) => TypedResults.Ok(); - Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - } - - private static IServiceProvider CreateServiceProvider() - { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddHttpContextAccessor(); - services.AddProblemDetails(); - return services.BuildServiceProvider(); - } - - private static TurboHttpContext CreateUrlEncodedContext(string path, string formData) - { - return ServerTestContext.Request() - .Post(path) - .FormBody(formData) - .Build(); - } - - private static TurboHttpContext CreateMultipartContext( - string path, string fieldName, string fileName, byte[] fileContent) - { - return ServerTestContext.Request() - .Post(path) - .MultipartBody(m => m.Add(new ByteArrayContent(fileContent), fieldName, fileName)) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/ParameterBinderSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/ParameterBinderSpec.cs deleted file mode 100644 index c6ac7625a..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/ParameterBinderSpec.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class ParameterBinderSpec -{ - [Fact(Timeout = 5000)] - public async Task ContextBinder_should_return_turbo_context() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - var binder = new ContextBinder(); - var result = await binder.BindAsync(ctx, null!); - Assert.Same(ctx, result); - } - - [Fact(Timeout = 5000)] - public async Task CancellationTokenBinder_should_return_request_aborted() - { - var cts = new CancellationTokenSource(); - var ctx = CreateContext("/test", cancellationToken: cts.Token); - var binder = new CancellationTokenBinder(); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(cts.Token, result); - } - - [Fact(Timeout = 5000)] - public async Task RouteValueBinder_should_extract_string() - { - var ctx = CreateContext("/orders/42", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "42"; - var binder = new RouteValueBinder("id", typeof(string)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal("42", result); - } - - [Fact(Timeout = 5000)] - public async Task RouteValueBinder_should_parse_int() - { - var ctx = CreateContext("/orders/42", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "42"; - var binder = new RouteValueBinder("id", typeof(int)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(42, result); - } - - [Fact(Timeout = 5000)] - public async Task RouteValueBinder_should_parse_guid() - { - var guid = Guid.NewGuid(); - var ctx = CreateContext("/items/" + guid, TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = guid.ToString(); - var binder = new RouteValueBinder("id", typeof(Guid)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(guid, result); - } - - [Fact(Timeout = 5000)] - public async Task ServiceBinder_should_resolve_from_di() - { - var services = new ServiceCollection(); - services.AddSingleton(new TestService()); - var provider = services.BuildServiceProvider(); - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - var binder = new ServiceBinder(typeof(ITestService)); - var result = await binder.BindAsync(ctx, provider); - Assert.IsType(result); - } - - [Fact(Timeout = 5000)] - public async Task QueryStringBinder_should_extract_string() - { - var ctx = CreateContext("/search?q=hello", TestContext.Current.CancellationToken); - var binder = new QueryStringBinder("q", typeof(string)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal("hello", result); - } - - [Fact(Timeout = 5000)] - public async Task QueryStringBinder_should_parse_int() - { - var ctx = CreateContext("/items?page=3", TestContext.Current.CancellationToken); - var binder = new QueryStringBinder("page", typeof(int)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(3, result); - } - - [Fact(Timeout = 5000)] - public async Task HeaderBinder_should_extract_string() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - ctx.Request.Headers["X-Tenant"] = "acme"; - var binder = new HeaderBinder("X-Tenant", typeof(string)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal("acme", result); - } - - [Fact(Timeout = 5000)] - public async Task HeaderBinder_should_parse_int() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - ctx.Request.Headers["X-Page-Size"] = "50"; - var binder = new HeaderBinder("X-Page-Size", typeof(int)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(50, result); - } - - [Fact(Timeout = 5000)] - public async Task HeaderBinder_should_return_null_when_header_missing() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - var binder = new HeaderBinder("X-Missing", typeof(string)); - var result = await binder.BindAsync(ctx, null!); - Assert.Null(result); - } - - [Fact(Timeout = 5000)] - public async Task HeaderBinder_should_return_default_for_value_type_when_missing() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - var binder = new HeaderBinder("X-Missing", typeof(int)); - var result = await binder.BindAsync(ctx, null!); - Assert.Equal(0, result); - } - - private interface ITestService; - - private sealed class TestService : ITestService; - - private static TurboHttpContext CreateContext(string path, CancellationToken cancellationToken = default) - { - return ServerTestContext.Request() - .Get(path) - .RequestAborted(cancellationToken) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/ParameterValidatorSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/ParameterValidatorSpec.cs deleted file mode 100644 index f0c8f9c2b..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/ParameterValidatorSpec.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using TurboHTTP.Routing.Binding; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class ParameterValidatorSpec -{ - [Fact(Timeout = 5000)] - public void Validate_should_pass_for_valid_object() - { - var dto = new ValidDto("Widget", 5); - var result = ParameterValidator.ValidateObject(dto, "body"); - Assert.True(result.IsValid); - } - - [Fact(Timeout = 5000)] - public void Validate_should_fail_for_missing_required_field() - { - var dto = new ValidDto(null!, 5); - var result = ParameterValidator.ValidateObject(dto, "body"); - Assert.False(result.IsValid); - Assert.Contains(result.Errors, e => e.Key == "Name"); - } - - [Fact(Timeout = 5000)] - public void Validate_should_fail_for_range_violation() - { - var dto = new ValidDto("Widget", 200); - var result = ParameterValidator.ValidateObject(dto, "body"); - Assert.False(result.IsValid); - Assert.Contains(result.Errors, e => e.Key == "Quantity"); - } - - [Fact(Timeout = 5000)] - public void Validate_should_pass_for_object_without_annotations() - { - var dto = new PlainDto("anything"); - var result = ParameterValidator.ValidateObject(dto, "body"); - Assert.True(result.IsValid); - } - - [Fact(Timeout = 5000)] - public void Validate_should_collect_multiple_errors() - { - var dto = new ValidDto(null!, 200); - var result = ParameterValidator.ValidateObject(dto, "body"); - Assert.False(result.IsValid); - Assert.True(result.Errors.Count >= 2); - } - - [Fact(Timeout = 5000)] - public void HasValidationAttributes_should_return_true_for_annotated_type() - { - Assert.True(ParameterValidator.HasValidationAttributes(typeof(ValidDto))); - } - - [Fact(Timeout = 5000)] - public void HasValidationAttributes_should_return_false_for_plain_type() - { - Assert.False(ParameterValidator.HasValidationAttributes(typeof(PlainDto))); - } - - public sealed record ValidDto( - [property: Required] string Name, - [property: Range(1, 100)] int Quantity); - - public sealed record PlainDto(string Name); -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/ParseErrorHandlingSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/ParseErrorHandlingSpec.cs deleted file mode 100644 index d4e5b089e..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/ParseErrorHandlingSpec.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class ParseErrorHandlingSpec -{ - [Fact(Timeout = 5000)] - public async Task RouteValueBinder_should_throw_on_invalid_int() - { - var ctx = CreateContext("/orders/invalid", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "invalid"; - var binder = new RouteValueBinder("id", typeof(int)); - await Assert.ThrowsAsync(() => binder.BindAsync(ctx, null!).AsTask()); - } - - [Fact(Timeout = 5000)] - public async Task RouteValueBinder_should_throw_on_invalid_guid() - { - var ctx = CreateContext("/items/invalid", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "not-a-guid"; - var binder = new RouteValueBinder("id", typeof(Guid)); - await Assert.ThrowsAsync(() => binder.BindAsync(ctx, null!).AsTask()); - } - - [Fact(Timeout = 5000)] - public async Task HeaderBinder_should_throw_on_invalid_int() - { - var ctx = CreateContext("/test", TestContext.Current.CancellationToken); - ctx.Request.Headers["X-Page-Size"] = "invalid"; - var binder = new HeaderBinder("X-Page-Size", typeof(int)); - await Assert.ThrowsAsync(() => binder.BindAsync(ctx, null!).AsTask()); - } - - [Fact(Timeout = 5000)] - public async Task QueryStringBinder_should_throw_on_invalid_int() - { - var ctx = CreateContext("/items?page=invalid", TestContext.Current.CancellationToken); - var binder = new QueryStringBinder("page", typeof(int)); - await Assert.ThrowsAsync(() => binder.BindAsync(ctx, null!).AsTask()); - } - - [Fact(Timeout = 5000)] - public async Task DelegateHandler_should_return_400_on_route_parse_error() - { - const string pattern = "/orders/{id}"; - var factory = DelegateHandlerBinder.Bind(pattern, Handler); - - var ctx = CreateContext("/orders/invalid", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "invalid"; - - await factory(ctx, null!); - - Assert.Equal(400, ctx.Response.StatusCode); - return; - Ok Handler(int id) => TypedResults.Ok(string.Concat("Order ", id)); - } - - [Fact(Timeout = 5000)] - public async Task DelegateHandler_should_return_400_on_query_parse_error() - { - var handler = (int page = 1) => - TypedResults.Ok(string.Concat("Page ", page)); - const string pattern = "/items"; - var factory = DelegateHandlerBinder.Bind(pattern, handler); - - var ctx = CreateContext("/items?page=invalid", TestContext.Current.CancellationToken); - - await factory(ctx, null!); - - Assert.Equal(400, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task DelegateHandler_should_return_400_on_header_parse_error() - { - var handler = ([FromHeader(Name = "X-Page-Size")] int pageSize) => - TypedResults.Ok(string.Concat("Size ", pageSize)); - var pattern = "/items"; - var factory = DelegateHandlerBinder.Bind(pattern, handler); - - var ctx = CreateContext("/items", TestContext.Current.CancellationToken); - ctx.Request.Headers["X-Page-Size"] = "invalid"; - - await factory(ctx, null!); - - Assert.Equal(400, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task EntityDelegate_should_throw_binding_validation_on_parse_error() - { - const string pattern = "/entities/{id}"; - var factory = DelegateHandlerBinder.BindEntityDelegate(pattern, Handler); - - var ctx = CreateContext("/entities/invalid", TestContext.Current.CancellationToken); - ctx.Request.RouteValues["id"] = "invalid"; - - var ex = await Assert.ThrowsAsync(() => factory(ctx, null!).AsTask()); - Assert.Equal(400, ex.StatusCode); - return; - GetEntityMessage Handler(int id) => new(id.ToString()); - } - - private sealed record GetEntityMessage(string Id); - - private static TurboHttpContext CreateContext(string path, CancellationToken cancellationToken = default) - { - return ServerTestContext.Request() - .Get(path) - .RequestAborted(cancellationToken) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Routing/Binding/RegistrationValidationSpec.cs b/src/TurboHTTP.Tests/Routing/Binding/RegistrationValidationSpec.cs deleted file mode 100644 index dd8df21a5..000000000 --- a/src/TurboHTTP.Tests/Routing/Binding/RegistrationValidationSpec.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TurboHTTP.Routing.Binding; - -namespace TurboHTTP.Tests.Routing.Binding; - -public sealed class RegistrationValidationSpec -{ - [Fact(Timeout = 5000)] - public void Bind_should_reject_multiple_FromBody_parameters() - { - Delegate handler = ([FromBody] CreateDto a, [FromBody] UpdateDto b) => TypedResults.Ok(); - var ex = Assert.Throws(() => DelegateHandlerBinder.Bind("/test", handler)); - Assert.Contains("FromBody", ex.Message); - } - - [Fact(Timeout = 5000)] - public void Bind_should_accept_single_FromBody_parameter() - { - Delegate handler = ([FromBody] CreateDto body) => TypedResults.Ok(); - var bound = DelegateHandlerBinder.Bind("/items", handler); - Assert.NotNull(bound); - } - - public sealed record CreateDto(string Name); - public sealed record UpdateDto(string Name); -} diff --git a/src/TurboHTTP.Tests/Routing/EndpointResolverSpec.cs b/src/TurboHTTP.Tests/Routing/EndpointResolverSpec.cs deleted file mode 100644 index cd86a4bf1..000000000 --- a/src/TurboHTTP.Tests/Routing/EndpointResolverSpec.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.Net.Security; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Servus.Akka.Transport; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Routing; - -public sealed class EndpointResolverSpec -{ - [Fact(Timeout = 5000)] - public void Resolve_should_produce_tcp_binding_for_http_listen() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(5000); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Single(bindings); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Equal("127.0.0.1", tcp.Host); - Assert.Equal((ushort)5000, tcp.Port); - Assert.Null(tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_produce_tcp_binding_with_cert_for_https_listen() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenLocalhost(5001, listen => - { - listen.UseHttps(cert); - }); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Single(bindings); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Same(cert, tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_set_alpn_from_protocols() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenLocalhost(5001, listen => - { - listen.UseHttps(cert); - listen.Protocols = HttpProtocols.Http2; - }); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.NotNull(tcp.ApplicationProtocols); - Assert.Single(tcp.ApplicationProtocols); - Assert.Equal(SslApplicationProtocol.Http2, tcp.ApplicationProtocols[0]); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_produce_two_bindings_when_http3_is_set() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenAnyIP(443, listen => - { - listen.UseHttps(cert); - listen.Protocols = HttpProtocols.Http1AndHttp2 | HttpProtocols.Http3; - }); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Equal(2, bindings.Count); - Assert.IsType(bindings[0].Options); - Assert.IsType(bindings[1].Options); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_share_certificate_between_tcp_and_quic_bindings() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenAnyIP(443, listen => - { - listen.UseHttps(cert); - listen.Protocols = HttpProtocols.Http1AndHttp2 | HttpProtocols.Http3; - }); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = (TcpListenerOptions)bindings[0].Options; - var quic = (QuicListenerOptions)bindings[1].Options; - Assert.Same(cert, tcp.ServerCertificate); - Assert.Same(cert, quic.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_apply_https_defaults_to_endpoints_with_use_https() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ConfigureHttpsDefaults(https => - { - https.ServerCertificate = cert; - }); - options.ListenLocalhost(443, listen => - { - listen.UseHttps(); - }); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Same(cert, tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_not_apply_https_defaults_to_plain_http_endpoints() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ConfigureHttpsDefaults(https => - { - https.ServerCertificate = cert; - }); - options.ListenLocalhost(80); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Null(tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_parse_http_url() - { - var options = new TurboServerOptions(); - options.Urls.Add("http://localhost:5000"); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Single(bindings); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Equal("127.0.0.1", tcp.Host); - Assert.Equal((ushort)5000, tcp.Port); - Assert.Null(tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_parse_https_url_with_defaults() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ConfigureHttpsDefaults(https => - { - https.ServerCertificate = cert; - }); - options.Urls.Add("https://localhost:5001"); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Single(bindings); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Same(cert, tcp.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_parse_wildcard_host_as_any_ip() - { - var options = new TurboServerOptions(); - options.Urls.Add("http://*:8080"); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Equal("0.0.0.0", tcp.Host); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_parse_ipv6_url() - { - var options = new TurboServerOptions(); - options.Urls.Add("http://[::1]:5000"); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Equal("::1", tcp.Host); - Assert.Equal((ushort)5000, tcp.Port); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_throw_for_https_url_without_certificate() - { - var options = new TurboServerOptions(); - options.Urls.Add("https://localhost:5001"); - - var ex = Assert.Throws(() => - new EndpointResolver().Resolve(options)); - Assert.Contains("No server certificate configured", ex.Message); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_throw_for_invalid_url() - { - var options = new TurboServerOptions(); - options.Urls.Add("not-a-url"); - - Assert.Throws(() => - new EndpointResolver().Resolve(options)); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_throw_for_unsupported_scheme() - { - var options = new TurboServerOptions(); - options.Urls.Add("ftp://localhost:21"); - - Assert.Throws(() => - new EndpointResolver().Resolve(options)); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_throw_for_http3_without_https() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(443, listen => - { - listen.Protocols = HttpProtocols.Http3; - }); - - var ex = Assert.Throws(() => - new EndpointResolver().Resolve(options)); - Assert.Contains("HTTP/3 requires HTTPS", ex.Message); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_throw_for_missing_cert_file() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(443, listen => - { - listen.UseHttps("nonexistent.pfx", "pw"); - }); - - Assert.Throws(() => - new EndpointResolver().Resolve(options)); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_include_raw_bind_endpoints() - { - var options = new TurboServerOptions(); - options.BindTcp("0.0.0.0", 9090); - options.ListenLocalhost(5000); - - var bindings = new EndpointResolver().Resolve(options); - - Assert.Equal(2, bindings.Count); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_set_handshake_timeout_on_tcp_options() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenLocalhost(443, listen => - { - listen.UseHttps(cert, https => - { - https.HandshakeTimeout = TimeSpan.FromSeconds(30); - }); - }); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Equal(TimeSpan.FromSeconds(30), tcp.HandshakeTimeout); - } - - [Fact(Timeout = 5000)] - public void Resolve_should_set_client_cert_callback_on_tcp_options() - { - using var cert = CreateSelfSignedCert(); - RemoteCertificateValidationCallback callback = (_, _, _, _) => true; - var options = new TurboServerOptions(); - options.ListenLocalhost(443, listen => - { - listen.UseHttps(cert, https => - { - https.ClientCertificateValidationCallback = callback; - }); - }); - - var bindings = new EndpointResolver().Resolve(options); - var tcp = Assert.IsType(bindings[0].Options); - Assert.Same(callback, tcp.ClientCertificateValidationCallback); - } - - private static X509Certificate2 CreateSelfSignedCert() - { - using var rsa = RSA.Create(2048); - var request = new CertificateRequest("CN=Test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - return request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); - } -} diff --git a/src/TurboHTTP.Tests/Routing/EntityDelegateBindingSpec.cs b/src/TurboHTTP.Tests/Routing/EntityDelegateBindingSpec.cs deleted file mode 100644 index 31f4a7cd6..000000000 --- a/src/TurboHTTP.Tests/Routing/EntityDelegateBindingSpec.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Akka.Actor; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Routing; - -public sealed class EntityDelegateBindingSpec -{ - [Fact(Timeout = 5000)] - public void OnGet_with_delegate_should_register_route() - { - var table = CreateApp(builder => - { - builder.OnGet((string id) => new GetEntityMessage(id)); - builder.UseResolver(); - }); - - var frozen = table.Freeze(); - var match = frozen.Match("GET", "/entities/42"); - Assert.True(match.IsMatch); - } - - [Fact(Timeout = 5000)] - public void OnPost_with_body_delegate_should_register_route() - { - var table = CreateApp(builder => - { - builder.OnPost((string id, [FromBody] CreateEntityDto body) => - new CreateEntityMessage(id, body.Name)); - builder.UseResolver(); - }); - - var frozen = table.Freeze(); - var match = frozen.Match("POST", "/entities/42"); - Assert.True(match.IsMatch); - } - - private sealed record GetEntityMessage(string Id); - - private sealed record CreateEntityMessage(string Id, string Name); - - private sealed record CreateEntityDto(string Name); - - private sealed class FakeResolver : IEntityActorResolver - { - public ValueTask ResolveAsync(IServiceProvider services, CancellationToken ct) - { - IActorRef? nobody = ActorRefs.Nobody; - return ValueTask.FromResult(nobody!); - } - } - - private static TurboRouteTable CreateApp(Action configure) - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - app.MapTurboEntity("/entities/{id}", configure); - return (app.Services.GetRequiredService()); - } -} diff --git a/src/TurboHTTP.Tests/Routing/EntityResponseMapperCollectionSpec.cs b/src/TurboHTTP.Tests/Routing/EntityResponseMapperCollectionSpec.cs deleted file mode 100644 index 4f5ad783f..000000000 --- a/src/TurboHTTP.Tests/Routing/EntityResponseMapperCollectionSpec.cs +++ /dev/null @@ -1,89 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Tests.Routing; - -public sealed class EntityResponseMapperCollectionSpec -{ - private record OrderResult(string Id); - - private sealed record DerivedResult(string Id) : OrderResult(Id); - - [Fact(Timeout = 5000)] - public async Task FindMapper_should_return_exact_type_match() - { - var collection = new EntityResponseMapperCollection(); - var invoked = false; - collection.Add((_, _) => - { - invoked = true; - return Task.CompletedTask; - }); - - var mapper = collection.FindMapper(typeof(OrderResult)); - Assert.NotNull(mapper); - await mapper(null!, new OrderResult("1")); - Assert.True(invoked); - } - - [Fact(Timeout = 5000)] - public void FindMapper_should_return_null_for_unregistered_type() - { - var collection = new EntityResponseMapperCollection(); - collection.Add((_, _) => Task.CompletedTask); - - var mapper = collection.FindMapper(typeof(string)); - Assert.Null(mapper); - } - - [Fact(Timeout = 5000)] - public async Task FindMapper_should_fall_back_to_assignable_match() - { - var collection = new EntityResponseMapperCollection(); - var capturedId = ""; - collection.Add((_, r) => - { - capturedId = r.Id; - return Task.CompletedTask; - }); - - var mapper = collection.FindMapper(typeof(DerivedResult)); - Assert.NotNull(mapper); - await mapper(null!, new DerivedResult("derived-1")); - Assert.Equal("derived-1", capturedId); - } - - [Fact(Timeout = 5000)] - public async Task FindMapper_should_prefer_exact_over_assignable() - { - var collection = new EntityResponseMapperCollection(); - var matched = ""; - collection.Add((_, _) => - { - matched = "base"; - return Task.CompletedTask; - }); - collection.Add((_, _) => - { - matched = "derived"; - return Task.CompletedTask; - }); - - var mapper = collection.FindMapper(typeof(DerivedResult)); - Assert.NotNull(mapper); - await mapper(null!, new DerivedResult("x")); - Assert.Equal("derived", matched); - } - - [Fact(Timeout = 5000)] - public void Count_should_reflect_registered_mappers() - { - var collection = new EntityResponseMapperCollection(); - Assert.Equal(0, collection.Count); - - collection.Add((_, _) => Task.CompletedTask); - Assert.Equal(1, collection.Count); - - collection.Add((_, _) => Task.CompletedTask); - Assert.Equal(2, collection.Count); - } -} diff --git a/src/TurboHTTP.Tests/Routing/RouteTableSpec.cs b/src/TurboHTTP.Tests/Routing/RouteTableSpec.cs deleted file mode 100644 index 237e1c479..000000000 --- a/src/TurboHTTP.Tests/Routing/RouteTableSpec.cs +++ /dev/null @@ -1,100 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Tests.Routing; - -public sealed class RouteTableSpec -{ - private static IRouteDispatcher Dummy() => new DelegateDispatcher(_ => Task.CompletedTask); - - [Fact(Timeout = 5000)] - public void Match_should_find_exact_static_route() - { - var table = new RouteTableBuilder() - .Add("GET", "/api/health", Dummy()) - .Build(); - - var result = table.Match("GET", "/api/health"); - Assert.True(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void Match_should_return_no_match_for_unknown_path() - { - var table = new RouteTableBuilder() - .Add("GET", "/api/health", Dummy()) - .Build(); - - var result = table.Match("GET", "/api/unknown"); - Assert.False(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void Match_should_extract_route_parameters() - { - var table = new RouteTableBuilder() - .Add("GET", "/api/orders/{id}", Dummy()) - .Build(); - - var result = table.Match("GET", "/api/orders/42"); - Assert.True(result.IsMatch); - Assert.Equal("42", result.RouteValues["id"]); - } - - [Fact(Timeout = 5000)] - public void Match_should_extract_multiple_parameters() - { - var table = new RouteTableBuilder() - .Add("GET", "/api/{controller}/{id}", Dummy()) - .Build(); - - var result = table.Match("GET", "/api/orders/42"); - Assert.True(result.IsMatch); - Assert.Equal("orders", result.RouteValues["controller"]); - Assert.Equal("42", result.RouteValues["id"]); - } - - [Fact(Timeout = 5000)] - public void Match_should_respect_http_method() - { - var table = new RouteTableBuilder() - .Add("POST", "/api/orders", Dummy()) - .Build(); - - var result = table.Match("GET", "/api/orders"); - Assert.False(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void Match_should_support_wildcard_method() - { - var table = new RouteTableBuilder() - .Add("*", "/api/health", Dummy()) - .Build(); - - Assert.True(table.Match("GET", "/api/health").IsMatch); - Assert.True(table.Match("POST", "/api/health").IsMatch); - } - - [Fact(Timeout = 5000)] - public void Match_should_prefer_static_over_parameterized() - { - var staticDispatcher = new DelegateDispatcher(_ => - { - // _ would be TurboHttpContext, setting StatusCode there - return Task.CompletedTask; - }); - var paramDispatcher = new DelegateDispatcher(_ => - { - return Task.CompletedTask; - }); - - var table = new RouteTableBuilder() - .Add("GET", "/api/orders/latest", staticDispatcher) - .Add("GET", "/api/orders/{id}", paramDispatcher) - .Build(); - - var result = table.Match("GET", "/api/orders/latest"); - Assert.True(result.IsMatch); - Assert.Same(staticDispatcher, result.Dispatcher); - } -} diff --git a/src/TurboHTTP.Tests/Routing/TurboEntityAskBuilderSpec.cs b/src/TurboHTTP.Tests/Routing/TurboEntityAskBuilderSpec.cs deleted file mode 100644 index 3f3d18ad4..000000000 --- a/src/TurboHTTP.Tests/Routing/TurboEntityAskBuilderSpec.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing; - -public sealed class TurboEntityAskBuilderSpec -{ - private sealed record OrderDto(string Id); - - private sealed record NotFoundResult; - - [Fact(Timeout = 5000)] - public void Mappers_should_be_empty_by_default() - { - var builder = new TurboEntityAskBuilder(); - Assert.Equal(0, builder.Mappers.Count); - } - - [Fact(Timeout = 5000)] - public void Response_should_register_mapper() - { - var builder = new TurboEntityAskBuilder(); - builder.Handle((_, _) => Task.CompletedTask); - Assert.Equal(1, builder.Mappers.Count); - } - - [Fact(Timeout = 5000)] - public async Task Response_should_invoke_handler_with_typed_response() - { - var capturedId = ""; - var builder = new TurboEntityAskBuilder(); - builder.Handle((_, order) => - { - capturedId = order.Id; - return Task.CompletedTask; - }); - - var mapper = builder.Mappers.FindMapper(typeof(OrderDto)); - Assert.NotNull(mapper); - await mapper(null!, new OrderDto("42")); - Assert.Equal("42", capturedId); - } - - [Fact(Timeout = 5000)] - public void Produces_should_register_mapper() - { - var builder = new TurboEntityAskBuilder(); - builder.Produces((_, _) => new TestResult(200)); - Assert.Equal(1, builder.Mappers.Count); - } - - [Fact(Timeout = 5000)] - public async Task Produces_should_execute_iresult() - { - var resultExecuted = false; - var builder = new TurboEntityAskBuilder(); - builder.Produces((_, _) => - { - resultExecuted = true; - return new TestResult(200); - }); - - var mapper = builder.Mappers.FindMapper(typeof(OrderDto)); - Assert.NotNull(mapper); - - var ctx = ServerTestContext.Request().Get("/") - .RequestAborted(TestContext.Current.CancellationToken).Build(); - await mapper(ctx, new OrderDto("1")); - Assert.True(resultExecuted); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void Response_and_Produces_should_coexist() - { - var builder = new TurboEntityAskBuilder(); - builder.Handle((_, _) => Task.CompletedTask); - builder.Produces((_, _) => new TestResult(404)); - - Assert.Equal(2, builder.Mappers.Count); - Assert.NotNull(builder.Mappers.FindMapper(typeof(OrderDto))); - Assert.NotNull(builder.Mappers.FindMapper(typeof(NotFoundResult))); - } - - [Fact(Timeout = 5000)] - public void WithTimeout_should_set_override() - { - var builder = new TurboEntityAskBuilder(); - builder.WithTimeout(TimeSpan.FromSeconds(42)); - Assert.Equal(TimeSpan.FromSeconds(42), builder.TimeoutOverride); - } - - [Fact(Timeout = 5000)] - public void TimeoutOverride_should_be_null_by_default() - { - var builder = new TurboEntityAskBuilder(); - Assert.Null(builder.TimeoutOverride); - } - - private sealed class TestResult(int statusCode) : IResult - { - public Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - } -} diff --git a/src/TurboHTTP.Tests/Routing/TurboEntityBuilderSpec.cs b/src/TurboHTTP.Tests/Routing/TurboEntityBuilderSpec.cs deleted file mode 100644 index 993a1a6f6..000000000 --- a/src/TurboHTTP.Tests/Routing/TurboEntityBuilderSpec.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Routing; - -public sealed class TurboEntityBuilderSpec -{ - private sealed class TestActorKey; - - private sealed record TestMessage(string Id); - - [Fact(Timeout = 5000)] - public void AddToRouteTable_should_register_get_route() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet((TurboHttpContext ctx) => new TestMessage(ctx.Request.RouteValues["id"]!.ToString()!)); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - var result = frozen.Match("GET", "/orders/42"); - Assert.True(result.IsMatch); - Assert.IsType(result.Dispatcher); - } - - [Fact(Timeout = 5000)] - public void AddToRouteTable_should_register_multiple_methods() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet(() => new TestMessage("get")); - builder.OnPut(() => new TestMessage("put")); - builder.OnDelete(() => new TestMessage("del")); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.True(frozen.Match("GET", "/orders/1").IsMatch); - Assert.True(frozen.Match("PUT", "/orders/1").IsMatch); - Assert.True(frozen.Match("DELETE", "/orders/1").IsMatch); - Assert.False(frozen.Match("POST", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void AddToRouteTable_should_extract_route_values() - { - var builder = new TurboEntityBuilder("/tenants/{tenantId}/orders/{orderId}"); - builder.OnGet(() => new TestMessage("get")); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - var result = frozen.Match("GET", "/tenants/t1/orders/o42"); - Assert.True(result.IsMatch); - Assert.Equal("t1", result.RouteValues["tenantId"]); - Assert.Equal("o42", result.RouteValues["orderId"]); - } - - [Fact(Timeout = 5000)] - public void AddToRouteTable_should_not_match_unregistered_method() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet(() => new TestMessage("get")); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.False(frozen.Match("POST", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void AddToRouteTable_should_coexist_with_delegate_routes() - { - var table = new TurboRouteTable(); - - table.Add("GET", "/health", ctx => - { - ctx.Response.StatusCode = 200; - return Task.CompletedTask; - }); - - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet(() => new TestMessage("get")); - builder.AddToRouteTable(table); - - var frozen = table.Freeze(); - - var healthResult = frozen.Match("GET", "/health"); - Assert.True(healthResult.IsMatch); - Assert.IsType(healthResult.Dispatcher); - - var orderResult = frozen.Match("GET", "/orders/42"); - Assert.True(orderResult.IsMatch); - Assert.IsType(orderResult.Dispatcher); - } - - [Fact(Timeout = 5000)] - public void Builder_should_accept_custom_resolver() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet(() => new TestMessage("get")); - builder.UseActorRef(); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.True(frozen.Match("GET", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void IsTell_should_set_tell_flag_in_config() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnPost(() => new TestMessage("new")).Tell(); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.True(frozen.Match("POST", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void IsTell_with_callback_should_register_tell_route() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnPost(() => new TestMessage("new")).Tell(tell => { tell.Produces(204); }); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.True(frozen.Match("POST", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void IsAsk_should_register_ask_route() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet(() => new TestMessage("get")).Ask(ask => - ask.Produces((_, _) => Results.NotFound()) - .Produces((_, _) => Results.Accepted())); - - var table = new TurboRouteTable(); - builder.AddToRouteTable(table); - var frozen = table.Freeze(); - - Assert.True(frozen.Match("GET", "/orders/1").IsMatch); - } - - [Fact(Timeout = 5000)] - public void IsAsk_without_handlers_should_throw() - { - var builder = new TurboEntityBuilder("/orders/{id}"); - var methodBuilder = builder.OnGet(() => new TestMessage("get")); - - Assert.Throws(() => - methodBuilder.Ask(_ => { })); - } -} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Routing/TurboEntityTellBuilderSpec.cs b/src/TurboHTTP.Tests/Routing/TurboEntityTellBuilderSpec.cs deleted file mode 100644 index 581505dc1..000000000 --- a/src/TurboHTTP.Tests/Routing/TurboEntityTellBuilderSpec.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Routing; - -public sealed class TurboEntityTellBuilderSpec -{ - private sealed class TestResult(int statusCode) : IResult - { - public Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - } - - [Fact(Timeout = 5000)] - public void ResponseHandler_should_be_null_by_default() - { - var builder = new TurboEntityTellBuilder(); - Assert.Null(builder.ResponseHandler); - } - - [Fact(Timeout = 5000)] - public async Task Response_with_status_code_should_set_status() - { - var builder = new TurboEntityTellBuilder(); - builder.Produces(204); - - var ctx = ServerTestContext.Request().Get("/") - .RequestAborted(TestContext.Current.CancellationToken).Build(); - await builder.ResponseHandler!(ctx); - - Assert.Equal(204, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Response_with_writer_should_set_status_and_invoke_writer() - { - var writerCalled = false; - var builder = new TurboEntityTellBuilder(); - builder.Handle(ctx => - { - ctx.Response.StatusCode = 202; - writerCalled = true; - return Task.CompletedTask; - }); - - var ctx = ServerTestContext.Request().Get("/") - .RequestAborted(TestContext.Current.CancellationToken).Build(); - await builder.ResponseHandler!(ctx); - - Assert.Equal(202, ctx.Response.StatusCode); - Assert.True(writerCalled); - } - - [Fact(Timeout = 5000)] - public async Task Produces_should_execute_iresult() - { - var builder = new TurboEntityTellBuilder(); - builder.Produces(_ => new TestResult(201)); - - var ctx = ServerTestContext.Request().Get("/") - .RequestAborted(TestContext.Current.CancellationToken).Build(); - await builder.ResponseHandler!(ctx); - - Assert.Equal(201, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Last_call_should_win() - { - var builder = new TurboEntityTellBuilder(); - builder.Produces(204); - builder.Produces(202); - - var ctx = ServerTestContext.Request().Get("/") - .RequestAborted(TestContext.Current.CancellationToken).Build(); - await builder.ResponseHandler!(ctx); - - Assert.Equal(202, ctx.Response.StatusCode); - } -} diff --git a/src/TurboHTTP.Tests/Routing/TurboRouteHandlerBuilderSpec.cs b/src/TurboHTTP.Tests/Routing/TurboRouteHandlerBuilderSpec.cs deleted file mode 100644 index 278c44788..000000000 --- a/src/TurboHTTP.Tests/Routing/TurboRouteHandlerBuilderSpec.cs +++ /dev/null @@ -1,59 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Routing; - -public sealed class TurboRouteHandlerBuilderSpec -{ - [Fact(Timeout = 5000)] - public void WithName_should_store_name() - { - var builder = new TurboRouteHandlerBuilder(); - var result = builder.WithName("GetUsers"); - Assert.Same(builder, result); - Assert.Equal("GetUsers", builder.Metadata.Name); - } - - [Fact(Timeout = 5000)] - public void WithTags_should_store_tags() - { - var builder = new TurboRouteHandlerBuilder(); - builder.WithTags("users", "admin"); - Assert.Equal(new[] { "users", "admin" }, builder.Metadata.Tags); - } - - [Fact(Timeout = 5000)] - public void WithMetadata_should_store_arbitrary_metadata() - { - var builder = new TurboRouteHandlerBuilder(); - builder.WithMetadata("key1", 42); - Assert.Contains("key1", builder.Metadata.Items); - Assert.Contains(42, builder.Metadata.Items); - } - - [Fact(Timeout = 5000)] - public void RequireAuthorization_should_set_flag() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization(); - Assert.True(builder.Metadata.RequiresAuthorization); - } - - [Fact(Timeout = 5000)] - public void AllowAnonymous_should_set_flag() - { - var builder = new TurboRouteHandlerBuilder(); - builder.AllowAnonymous(); - Assert.True(builder.Metadata.AllowsAnonymous); - } - - [Fact(Timeout = 5000)] - public void Fluent_chaining_should_return_same_instance() - { - var builder = new TurboRouteHandlerBuilder(); - var result = builder - .WithName("test") - .WithTags("t1") - .RequireAuthorization(); - Assert.Same(builder, result); - } -} diff --git a/src/TurboHTTP.Tests/Routing/TurboRouteTableSpec.cs b/src/TurboHTTP.Tests/Routing/TurboRouteTableSpec.cs deleted file mode 100644 index d31a24460..000000000 --- a/src/TurboHTTP.Tests/Routing/TurboRouteTableSpec.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Routing; - -public sealed class TurboRouteTableSpec -{ - [Fact(Timeout = 5000)] - public void Add_should_register_route() - { - var table = new TurboRouteTable(); - table.Add("GET", "/health", _ => Results.Ok().ExecuteAsync(null!)); - var frozen = table.Freeze(); - var result = frozen.Match("GET", "/health"); - Assert.True(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void Freeze_should_return_same_instance_on_second_call() - { - var table = new TurboRouteTable(); - table.Add("GET", "/test", _ => Results.Ok().ExecuteAsync(null!)); - var first = table.Freeze(); - var second = table.Freeze(); - Assert.Same(first, second); - } - - [Fact(Timeout = 5000)] - public void Group_should_prepend_prefix() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api/v1"); - group.MapGet("/users", () => Results.Ok()); - var frozen = table.Freeze(); - var result = frozen.Match("GET", "/api/v1/users"); - Assert.True(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void Nested_groups_should_concat_prefixes() - { - var table = new TurboRouteTable(); - var api = table.CreateGroup("/api"); - var v1 = api.MapGroup("/v1"); - v1.MapGet("/items", () => Results.Ok()); - var frozen = table.Freeze(); - var result = frozen.Match("GET", "/api/v1/items"); - Assert.True(result.IsMatch); - } - - [Fact(Timeout = 5000)] - public void RouteBuilder_should_return_from_map_methods() - { - var table = new TurboRouteTable(); - var builder = table.Add("GET", "/test", _ => Results.Ok().ExecuteAsync(null!)); - Assert.NotNull(builder); - Assert.IsType(builder); - } -} diff --git a/src/TurboHTTP.Tests/Context/Features/TlsHandshakeFeatureSpec.cs b/src/TurboHTTP.Tests/Server/Context/Features/TlsHandshakeFeatureSpec.cs similarity index 95% rename from src/TurboHTTP.Tests/Context/Features/TlsHandshakeFeatureSpec.cs rename to src/TurboHTTP.Tests/Server/Context/Features/TlsHandshakeFeatureSpec.cs index 69cf0cdce..f03f86827 100644 --- a/src/TurboHTTP.Tests/Context/Features/TlsHandshakeFeatureSpec.cs +++ b/src/TurboHTTP.Tests/Server/Context/Features/TlsHandshakeFeatureSpec.cs @@ -1,8 +1,8 @@ using System.Net.Security; using System.Security.Authentication; -using TurboHTTP.Context.Features; +using TurboHTTP.Server.Context.Features; -namespace TurboHTTP.Tests.Context.Features; +namespace TurboHTTP.Tests.Server.Context.Features; public sealed class TlsHandshakeFeatureSpec { diff --git a/src/TurboHTTP.Tests/Context/Features/TurboFeatureCollectionSpec.cs b/src/TurboHTTP.Tests/Server/Context/Features/TurboFeatureCollectionSpec.cs similarity index 66% rename from src/TurboHTTP.Tests/Context/Features/TurboFeatureCollectionSpec.cs rename to src/TurboHTTP.Tests/Server/Context/Features/TurboFeatureCollectionSpec.cs index 50bdcbe58..1af7dcb7d 100644 --- a/src/TurboHTTP.Tests/Context/Features/TurboFeatureCollectionSpec.cs +++ b/src/TurboHTTP.Tests/Server/Context/Features/TurboFeatureCollectionSpec.cs @@ -1,9 +1,8 @@ using System.Net; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; -namespace TurboHTTP.Tests.Context.Features; +namespace TurboHTTP.Tests.Server.Context.Features; public sealed class TurboFeatureCollectionSpec { @@ -11,7 +10,7 @@ public sealed class TurboFeatureCollectionSpec public void Get_should_return_null_for_unset_feature() { var collection = new TurboFeatureCollection(); - Assert.Null(collection.Get()); + Assert.Null(collection.Get()); } [Fact(Timeout = 5000)] @@ -19,8 +18,8 @@ public void Set_and_Get_should_round_trip_for_request_feature() { var collection = new TurboFeatureCollection(); var feature = new TurboHttpRequestFeature(); - collection.Set(feature); - Assert.Same(feature, collection.Get()); + collection.Set(feature); + Assert.Same(feature, collection.Get()); } [Fact(Timeout = 5000)] @@ -28,23 +27,24 @@ public void Set_and_Get_should_round_trip_for_response_feature() { var collection = new TurboFeatureCollection(); var feature = new TurboHttpResponseFeature(); - collection.Set(feature); - Assert.Same(feature, collection.Get()); + collection.Set(feature); + Assert.Same(feature, collection.Get()); } [Fact(Timeout = 5000)] public void Set_and_Get_should_round_trip_for_connection_feature() { var collection = new TurboFeatureCollection(); - var info = new TurboConnectionInfo( - "test-connection", - IPAddress.Loopback, - 12345, - IPAddress.Loopback, - 80); - var feature = new TurboHttpConnectionFeature(info); - collection.Set(feature); - Assert.Same(feature, collection.Get()); + var feature = new TurboHttpConnectionFeature + { + ConnectionId = "test-connection", + RemoteIpAddress = IPAddress.Loopback, + RemotePort = 12345, + LocalIpAddress = IPAddress.Loopback, + LocalPort = 80 + }; + collection.Set(feature); + Assert.Same(feature, collection.Get()); } [Fact(Timeout = 5000)] @@ -52,9 +52,9 @@ public void Set_null_should_clear_feature() { var collection = new TurboFeatureCollection(); var feature = new TurboHttpRequestFeature(); - collection.Set(feature); - collection.Set(null); - Assert.Null(collection.Get()); + collection.Set(feature); + collection.Set(null); + Assert.Null(collection.Get()); } [Fact(Timeout = 5000)] @@ -76,17 +76,6 @@ public void IFeatureCollection_Get_should_work_for_aspnet_interfaces() Assert.Same(feature, fc.Get()); } - [Fact(Timeout = 5000)] - public void Same_feature_registered_under_both_interfaces_should_be_retrievable_by_either() - { - var collection = new TurboFeatureCollection(); - var feature = new TurboHttpRequestFeature(); - collection.Set(feature); - collection.Set(feature); - Assert.Same(feature, collection.Get()); - Assert.Same(feature, collection.Get()); - } - [Fact(Timeout = 5000)] public void IFeatureCollection_indexer_should_work() { diff --git a/src/TurboHTTP.Tests/Server/Context/TurboHttpConnectionFeatureSpec.cs b/src/TurboHTTP.Tests/Server/Context/TurboHttpConnectionFeatureSpec.cs new file mode 100644 index 000000000..acdd45641 --- /dev/null +++ b/src/TurboHTTP.Tests/Server/Context/TurboHttpConnectionFeatureSpec.cs @@ -0,0 +1,38 @@ +using System.Net; +using TurboHTTP.Server.Context.Features; + +namespace TurboHTTP.Tests.Server.Context; + +public sealed class TurboHttpConnectionFeatureSpec +{ + [Fact(Timeout = 5000)] + public void ConnectionId_should_store_connection_id() + { + var feature = new TurboHttpConnectionFeature { ConnectionId = "conn-42" }; + Assert.Equal("conn-42", feature.ConnectionId); + } + + [Fact(Timeout = 5000)] + public void RemoteIpAddress_should_store_remote_endpoint() + { + var feature = new TurboHttpConnectionFeature + { + RemoteIpAddress = IPAddress.Parse("10.0.0.1"), + RemotePort = 9999 + }; + Assert.Equal(IPAddress.Parse("10.0.0.1"), feature.RemoteIpAddress); + Assert.Equal(9999, feature.RemotePort); + } + + [Fact(Timeout = 5000)] + public void LocalEndpoint_should_store_local_endpoint() + { + var feature = new TurboHttpConnectionFeature + { + LocalIpAddress = IPAddress.Parse("192.168.1.1"), + LocalPort = 8080 + }; + Assert.Equal(IPAddress.Parse("192.168.1.1"), feature.LocalIpAddress); + Assert.Equal(8080, feature.LocalPort); + } +} diff --git a/src/TurboHTTP.Tests/Context/TurboHttpRequestFeatureSpec.cs b/src/TurboHTTP.Tests/Server/Context/TurboHttpRequestFeatureSpec.cs similarity index 97% rename from src/TurboHTTP.Tests/Context/TurboHttpRequestFeatureSpec.cs rename to src/TurboHTTP.Tests/Server/Context/TurboHttpRequestFeatureSpec.cs index a53adcea7..f0dd9c8e3 100644 --- a/src/TurboHTTP.Tests/Context/TurboHttpRequestFeatureSpec.cs +++ b/src/TurboHTTP.Tests/Server/Context/TurboHttpRequestFeatureSpec.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; +using TurboHTTP.Server.Context.Features; -namespace TurboHTTP.Tests.Context; +namespace TurboHTTP.Tests.Server.Context; public sealed class TurboHttpRequestFeatureSpec { diff --git a/src/TurboHTTP.Tests/Context/TurboHttpResponseBodyFeatureSpec.cs b/src/TurboHTTP.Tests/Server/Context/TurboHttpResponseBodyFeatureSpec.cs similarity index 98% rename from src/TurboHTTP.Tests/Context/TurboHttpResponseBodyFeatureSpec.cs rename to src/TurboHTTP.Tests/Server/Context/TurboHttpResponseBodyFeatureSpec.cs index 07c83506f..f9353a330 100644 --- a/src/TurboHTTP.Tests/Context/TurboHttpResponseBodyFeatureSpec.cs +++ b/src/TurboHTTP.Tests/Server/Context/TurboHttpResponseBodyFeatureSpec.cs @@ -2,9 +2,9 @@ using Akka.Actor; using Akka.Streams; using Akka.Streams.Dsl; -using TurboHTTP.Context.Features; +using TurboHTTP.Server.Context.Features; -namespace TurboHTTP.Tests.Context; +namespace TurboHTTP.Tests.Server.Context; public sealed class TurboHttpResponseBodyFeatureSpec : IDisposable { diff --git a/src/TurboHTTP.Tests/Context/TurboHttpResponseFeatureSpec.cs b/src/TurboHTTP.Tests/Server/Context/TurboHttpResponseFeatureSpec.cs similarity index 96% rename from src/TurboHTTP.Tests/Context/TurboHttpResponseFeatureSpec.cs rename to src/TurboHTTP.Tests/Server/Context/TurboHttpResponseFeatureSpec.cs index 503aac1d7..f89796cf5 100644 --- a/src/TurboHTTP.Tests/Context/TurboHttpResponseFeatureSpec.cs +++ b/src/TurboHTTP.Tests/Server/Context/TurboHttpResponseFeatureSpec.cs @@ -1,6 +1,6 @@ -using TurboHTTP.Context.Features; +using TurboHTTP.Server.Context.Features; -namespace TurboHTTP.Tests.Context; +namespace TurboHTTP.Tests.Server.Context; public sealed class TurboHttpResponseFeatureSpec { diff --git a/src/TurboHTTP.Tests/Server/ContextPoolingSpec.cs b/src/TurboHTTP.Tests/Server/ContextPoolingSpec.cs index 603ad4b47..0111c6685 100644 --- a/src/TurboHTTP.Tests/Server/ContextPoolingSpec.cs +++ b/src/TurboHTTP.Tests/Server/ContextPoolingSpec.cs @@ -1,36 +1,19 @@ -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Features; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Tests.Server; public sealed class ContextPoolingSpec { - private static TurboHttpContext CreateContext(IFeatureCollection? features = null) + private static IFeatureCollection CreateContext(IFeatureCollection? features = null) { features ??= new FeatureCollection(); features.Set(new TurboHttpRequestFeature()); features.Set(new TurboHttpResponseFeature()); features.Set(new TurboHttpResponseBodyFeature()); - features.Set(new TurboRequestBodyFeature()); - - var connectionInfo = new TurboConnectionInfo( - "test-id", - null, - 0, - null, - 0); - - var ctx = new TurboHttpContext( - features, - connectionInfo, - services: null, - requestAborted: CancellationToken.None, - materializer: null!); - return ctx; + return features; } [Fact(Timeout = 5000)] @@ -104,102 +87,18 @@ public void TurboHttpResponseFeature_Reset_clears_has_started() } [Fact(Timeout = 5000)] - public void TurboHttpContext_Reset_clears_user() - { - var ctx = CreateContext(); - ctx.User = new System.Security.Claims.ClaimsPrincipal(); - - var newFeatures = new FeatureCollection(); - newFeatures.Set(new TurboHttpRequestFeature()); - newFeatures.Set(new TurboHttpResponseFeature()); - newFeatures.Set(new TurboHttpResponseBodyFeature()); - newFeatures.Set(new TurboRequestBodyFeature()); - - var newConnectionInfo = new TurboConnectionInfo("new-id", null, 0, null, 0); - ctx.Reset(newFeatures, newConnectionInfo, null, CancellationToken.None, null!); - - Assert.NotNull(ctx.User); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_Reset_clears_items() - { - var ctx = CreateContext(); - ctx.Items["key"] = "value"; - - var newFeatures = new FeatureCollection(); - newFeatures.Set(new TurboHttpRequestFeature()); - newFeatures.Set(new TurboHttpResponseFeature()); - newFeatures.Set(new TurboHttpResponseBodyFeature()); - newFeatures.Set(new TurboRequestBodyFeature()); - - var newConnectionInfo = new TurboConnectionInfo("new-id", null, 0, null, 0); - ctx.Reset(newFeatures, newConnectionInfo, null, CancellationToken.None, null!); - - Assert.Empty(ctx.Items); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_Reset_clears_trace_identifier() - { - var ctx = CreateContext(); - _ = ctx.TraceIdentifier; - - var newFeatures = new FeatureCollection(); - newFeatures.Set(new TurboHttpRequestFeature()); - newFeatures.Set(new TurboHttpResponseFeature()); - newFeatures.Set(new TurboHttpResponseBodyFeature()); - newFeatures.Set(new TurboRequestBodyFeature()); - - var newConnectionInfo = new TurboConnectionInfo("new-id", null, 0, null, 0); - ctx.Reset(newFeatures, newConnectionInfo, null, CancellationToken.None, null!); - - Assert.NotEqual("", ctx.TraceIdentifier); - } - - [Fact(Timeout = 5000)] - public void TurboHttpRequest_Reset_clears_cached_uri() - { - var features = new TurboFeatureCollection(); - var headers = new HeaderDictionary { { "Host", "example.com" } }; - var requestFeature = new TurboHttpRequestFeature { Scheme = "https", Path = "/api", Headers = headers }; - features.Set(requestFeature); - features.Set(new TurboHttpResponseFeature()); - features.Set(new TurboHttpResponseBodyFeature()); - features.Set(new TurboRequestBodyFeature()); - - var request = new TurboHttpRequest(features); - var originalUri = request.RequestUri; - Assert.NotNull(originalUri); - Assert.Equal("https://example.com/api", originalUri.ToString()); - - var newHeaders = new HeaderDictionary { { "Host", "different.com" } }; - var newFeatures = new TurboFeatureCollection(); - newFeatures.Set(new TurboHttpRequestFeature { Scheme = "http", Path = "/test", Headers = newHeaders }); - newFeatures.Set(new TurboHttpResponseFeature()); - newFeatures.Set(new TurboHttpResponseBodyFeature()); - newFeatures.Set(new TurboRequestBodyFeature()); - - request.Reset(newFeatures); - - var newUri = request.RequestUri; - Assert.NotNull(newUri); - Assert.Equal("http://different.com/test", newUri.ToString()); - } - - [Fact(Timeout = 5000)] - public void ServerContextFactory_Return_stores_context_in_pool() + public void FeatureCollectionFactory_Return_stores_context_in_pool() { var ctx = CreateContext(); - ServerContextFactory.Return(ctx); + FeatureCollectionFactory.Return(ctx); - var ctx2 = ServerContextFactory.Create( + var ctx2 = FeatureCollectionFactory.Create( new TurboHttpRequestFeature(), hasBody: false, services: null, - connectionInfo: new TurboConnectionInfo("id", null, 0, null, 0)); + connectionFeature: null); Assert.NotNull(ctx2); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Server/HttpProtocolsSpec.cs b/src/TurboHTTP.Tests/Server/HttpProtocolsSpec.cs deleted file mode 100644 index 40dbada99..000000000 --- a/src/TurboHTTP.Tests/Server/HttpProtocolsSpec.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Net.Security; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class HttpProtocolsSpec -{ - [Fact(Timeout = 5000)] - public void Http1_should_have_value_1() - { - Assert.Equal(1, (int)HttpProtocols.Http1); - } - - [Fact(Timeout = 5000)] - public void Http2_should_have_value_2() - { - Assert.Equal(2, (int)HttpProtocols.Http2); - } - - [Fact(Timeout = 5000)] - public void Http1AndHttp2_should_combine_Http1_and_Http2() - { - Assert.Equal(HttpProtocols.Http1 | HttpProtocols.Http2, HttpProtocols.Http1AndHttp2); - } - - [Fact(Timeout = 5000)] - public void Http3_should_have_value_4() - { - Assert.Equal(4, (int)HttpProtocols.Http3); - } - - [Fact(Timeout = 5000)] - public void ToAlpnProtocols_should_return_http11_for_Http1() - { - var result = HttpProtocols.Http1.ToAlpnProtocols(); - Assert.Single(result); - Assert.Equal(SslApplicationProtocol.Http11, result[0]); - } - - [Fact(Timeout = 5000)] - public void ToAlpnProtocols_should_return_h2_for_Http2() - { - var result = HttpProtocols.Http2.ToAlpnProtocols(); - Assert.Single(result); - Assert.Equal(SslApplicationProtocol.Http2, result[0]); - } - - [Fact(Timeout = 5000)] - public void ToAlpnProtocols_should_return_h2_then_http11_for_Http1AndHttp2() - { - var result = HttpProtocols.Http1AndHttp2.ToAlpnProtocols(); - Assert.Equal(2, result.Count); - Assert.Equal(SslApplicationProtocol.Http2, result[0]); - Assert.Equal(SslApplicationProtocol.Http11, result[1]); - } - - [Fact(Timeout = 5000)] - public void ToAlpnProtocols_should_return_h3_for_Http3() - { - var result = HttpProtocols.Http3.ToAlpnProtocols(); - Assert.Single(result); - Assert.Equal(new SslApplicationProtocol("h3"), result[0]); - } - - [Fact(Timeout = 5000)] - public void ToAlpnProtocols_should_return_empty_for_None() - { - var result = HttpProtocols.None.ToAlpnProtocols(); - Assert.Empty(result); - } -} diff --git a/src/TurboHTTP.Tests/Server/HttpsDefaultsSpec.cs b/src/TurboHTTP.Tests/Server/HttpsDefaultsSpec.cs deleted file mode 100644 index d42190f3a..000000000 --- a/src/TurboHTTP.Tests/Server/HttpsDefaultsSpec.cs +++ /dev/null @@ -1,43 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class HttpsDefaultsSpec -{ - [Fact(Timeout = 5000)] - public void ConfigureHttpsDefaults_should_store_callback() - { - var options = new TurboServerOptions(); - - options.ConfigureHttpsDefaults(https => - { - https.HandshakeTimeout = TimeSpan.FromSeconds(15); - }); - - Assert.NotNull(options.HttpsDefaultsCallback); - } - - [Fact(Timeout = 5000)] - public void ConfigureHttpsDefaults_callback_should_apply_to_options() - { - var options = new TurboServerOptions(); - - options.ConfigureHttpsDefaults(https => - { - https.HandshakeTimeout = TimeSpan.FromSeconds(42); - }); - - var httpsOptions = new TurboHttpsOptions(); - options.HttpsDefaultsCallback!.Invoke(httpsOptions); - - Assert.Equal(TimeSpan.FromSeconds(42), httpsOptions.HandshakeTimeout); - } - - [Fact(Timeout = 5000)] - public void ConfigureHttpsDefaults_should_be_null_by_default() - { - var options = new TurboServerOptions(); - - Assert.Null(options.HttpsDefaultsCallback); - } -} diff --git a/src/TurboHTTP.Tests/Server/ProtocolOptionsDefaultsSpec.cs b/src/TurboHTTP.Tests/Server/ProtocolOptionsDefaultsSpec.cs deleted file mode 100644 index 120bebd0d..000000000 --- a/src/TurboHTTP.Tests/Server/ProtocolOptionsDefaultsSpec.cs +++ /dev/null @@ -1,74 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class ProtocolOptionsDefaultsSpec -{ - [Fact(Timeout = 5000)] - public void Http1ServerOptions_should_have_correct_defaults() - { - var opts = new Http1ServerOptions(); - - Assert.Equal(8192, opts.MaxRequestLineLength); - Assert.Equal(8192, opts.MaxRequestTargetLength); - Assert.Equal(16, opts.MaxPipelinedRequests); - Assert.Equal(4096, opts.MaxChunkExtensionLength); - Assert.Equal(TimeSpan.FromSeconds(30), opts.BodyReadTimeout); - Assert.Equal(30_000_000, opts.MaxRequestBodySize); - Assert.Equal(32 * 1024, opts.MaxHeaderListSize); - Assert.Null(opts.KeepAliveTimeout); - Assert.Null(opts.RequestHeadersTimeout); - } - - [Fact(Timeout = 5000)] - public void Http2ServerOptions_should_have_correct_defaults() - { - var opts = new Http2ServerOptions(); - - Assert.Equal(100, opts.MaxConcurrentStreams); - Assert.Equal(1 * 1024 * 1024, opts.InitialConnectionWindowSize); - Assert.Equal(768 * 1024, opts.InitialStreamWindowSize); - Assert.Equal(16 * 1024, opts.MaxFrameSize); - Assert.Equal(32 * 1024, opts.MaxHeaderListSize); - Assert.Equal(4 * 1024, opts.HeaderTableSize); - Assert.Equal(30_000_000, opts.MaxRequestBodySize); - Assert.Equal(64 * 1024, opts.MaxResponseBufferSize); - Assert.Equal(TimeSpan.FromSeconds(130), opts.KeepAliveTimeout); - Assert.Equal(TimeSpan.FromSeconds(30), opts.RequestHeadersTimeout); - Assert.Equal(240, opts.MinRequestBodyDataRate); - Assert.Equal(TimeSpan.FromSeconds(5), opts.MinRequestBodyDataRateGracePeriod); - } - - [Fact(Timeout = 5000)] - public void Http3ServerOptions_should_have_correct_defaults() - { - var opts = new Http3ServerOptions(); - - Assert.Equal(100, opts.MaxConcurrentStreams); - Assert.Equal(32 * 1024, opts.MaxHeaderListSize); - Assert.Equal(0, opts.QpackMaxTableCapacity); - Assert.False(opts.EnableWebTransport); - Assert.Equal(30_000_000, opts.MaxRequestBodySize); - Assert.Equal(TimeSpan.FromSeconds(130), opts.KeepAliveTimeout); - Assert.Equal(TimeSpan.FromSeconds(30), opts.RequestHeadersTimeout); - Assert.Equal(240, opts.MinRequestBodyDataRate); - Assert.Equal(TimeSpan.FromSeconds(5), opts.MinRequestBodyDataRateGracePeriod); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_nest_protocol_options() - { - var opts = new TurboServerOptions(); - - Assert.NotNull(opts.Http1); - Assert.NotNull(opts.Http2); - Assert.NotNull(opts.Http3); - Assert.Equal(65536, opts.BodyBufferThreshold); - Assert.Equal(16384, opts.ResponseBodyChunkSize); - Assert.Equal(TimeSpan.FromSeconds(130), opts.KeepAliveTimeout); - Assert.Equal(TimeSpan.FromSeconds(30), opts.RequestHeadersTimeout); - Assert.Equal(TimeSpan.FromSeconds(30), opts.GracefulShutdownTimeout); - Assert.Equal(TimeSpan.FromSeconds(30), opts.BodyConsumptionTimeout); - Assert.Equal(0, opts.MaxConcurrentConnections); - } -} diff --git a/src/TurboHTTP.Tests/Server/Routing/RouteMetadataSpec.cs b/src/TurboHTTP.Tests/Server/Routing/RouteMetadataSpec.cs deleted file mode 100644 index dd3716327..000000000 --- a/src/TurboHTTP.Tests/Server/Routing/RouteMetadataSpec.cs +++ /dev/null @@ -1,281 +0,0 @@ -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server.Routing; - -public sealed class RouteMetadataSpec -{ - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_RequireAuthorization_with_policy_should_add_authorize_data() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization("Admin"); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Equal("Admin", authData.Policy); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_RequireAuthorization_with_null_policy_should_add_authorize_data() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization(null); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Null(authData.Policy); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_RequireAuthorization_multiple_policies_should_accumulate() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization("Policy1"); - builder.RequireAuthorization("Policy2"); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetOrderedMetadata().ToList(); - Assert.Equal(2, authData.Count); - Assert.Equal("Policy1", authData[0].Policy); - Assert.Equal("Policy2", authData[1].Policy); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_WithDisplayName_should_set_display_name() - { - var builder = new TurboRouteHandlerBuilder(); - builder.WithDisplayName("Get Users"); - - Assert.Equal("Get Users", builder.Metadata.DisplayName); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_WithTags_should_create_tags_metadata() - { - var builder = new TurboRouteHandlerBuilder(); - builder.WithTags("api", "v2"); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var tags = metadata.GetMetadata(); - Assert.NotNull(tags); - Assert.Equal(2, tags.Tags.Count); - Assert.Equal("api", tags.Tags[0]); - Assert.Equal("v2", tags.Tags[1]); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_AllowAnonymous_should_add_marker() - { - var builder = new TurboRouteHandlerBuilder(); - builder.AllowAnonymous(); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - Assert.True(metadata.HasMetadata()); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_WithMetadata_should_preserve_custom_objects() - { - var custom = new CustomMeta("value"); - var builder = new TurboRouteHandlerBuilder(); - builder.WithMetadata(custom); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var retrieved = metadata.GetMetadata(); - Assert.Same(custom, retrieved); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_empty_should_return_null_metadata() - { - var builder = new TurboRouteHandlerBuilder(); - var metadata = builder.BuildMetadata(); - Assert.Null(metadata); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_legacy_RequireAuthorization_without_policy_should_work() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization(); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Null(authData.Policy); - } - - [Fact(Timeout = 5000)] - public void RouteHandlerBuilder_RequireAuthorization_without_policy_followed_by_with_policy_uses_only_policy() - { - var builder = new TurboRouteHandlerBuilder(); - builder.RequireAuthorization(); - builder.RequireAuthorization("Admin"); - - var metadata = builder.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetOrderedMetadata().ToList(); - Assert.Single(authData); - Assert.Equal("Admin", authData[0].Policy); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_WithTags_should_apply_to_routes() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.WithTags("api", "v1"); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - var tags = metadata.GetMetadata(); - Assert.NotNull(tags); - Assert.Equal(2, tags.Tags.Count); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_RequireAuthorization_with_policy_should_apply_to_routes() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.RequireAuthorization("Admin"); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Equal("Admin", authData.Policy); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_RequireAuthorization_without_policy_should_apply_to_routes() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.RequireAuthorization(); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Null(authData.Policy); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_AllowAnonymous_should_apply_to_routes() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.AllowAnonymous(); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - Assert.True(metadata.HasMetadata()); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_WithMetadata_should_apply_to_routes() - { - var custom = new CustomMeta("group-value"); - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.WithMetadata(custom); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - var retrieved = metadata.GetMetadata(); - Assert.Same(custom, retrieved); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_multiple_WithTags_should_accumulate() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.WithTags("tag1"); - group.WithTags("tag2"); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - var tags = metadata.GetMetadata(); - Assert.NotNull(tags); - Assert.Equal(2, tags.Tags.Count); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_group_and_route_metadata_should_merge() - { - var table = new TurboRouteTable(); - var group = table.CreateGroup("/api"); - group.WithTags("group-tag"); - group.RequireAuthorization("GroupPolicy"); - - var handler = group.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - handler.RequireAuthorization("RoutePolicy"); - - var metadata = handler.BuildMetadata(); - Assert.NotNull(metadata); - - var authData = metadata.GetOrderedMetadata().ToList(); - Assert.Equal(2, authData.Count); - Assert.Equal("GroupPolicy", authData[0].Policy); - Assert.Equal("RoutePolicy", authData[1].Policy); - - var tags = metadata.GetMetadata(); - Assert.NotNull(tags); - Assert.Single(tags.Tags); - Assert.Equal("group-tag", tags.Tags[0]); - } - - [Fact(Timeout = 5000)] - public void RouteGroupBuilder_nested_groups_should_merge_metadata() - { - var table = new TurboRouteTable(); - var group1 = table.CreateGroup("/api"); - group1.WithTags("api"); - group1.RequireAuthorization("Admin"); - - var group2 = group1.MapGroup("/v1"); - group2.WithTags("v1"); - - var handler = group2.MapGet("/users", () => Microsoft.AspNetCore.Http.Results.Ok()); - var metadata = handler.BuildMetadata(); - - Assert.NotNull(metadata); - - var authData = metadata.GetMetadata(); - Assert.NotNull(authData); - Assert.Equal("Admin", authData.Policy); - } - - private sealed record CustomMeta(string Value); -} diff --git a/src/TurboHTTP.Tests/Server/Routing/TurboEndpointMetadataSpec.cs b/src/TurboHTTP.Tests/Server/Routing/TurboEndpointMetadataSpec.cs deleted file mode 100644 index 2336fee67..000000000 --- a/src/TurboHTTP.Tests/Server/Routing/TurboEndpointMetadataSpec.cs +++ /dev/null @@ -1,93 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Tests.Server.Routing; - -public sealed class TurboEndpointMetadataSpec -{ - [Fact(Timeout = 5000)] - public void GetMetadata_should_return_null_when_empty() - { - var metadata = new TurboEndpointMetadata([]); - Assert.Null(metadata.GetMetadata()); - } - - [Fact(Timeout = 5000)] - public void GetMetadata_should_return_matching_item() - { - var auth = new AuthorizeData("AdminPolicy", null, null); - var metadata = new TurboEndpointMetadata([auth]); - Assert.Same(auth, metadata.GetMetadata()); - } - - [Fact(Timeout = 5000)] - public void HasMetadata_should_return_true_when_present() - { - var metadata = new TurboEndpointMetadata([new AllowAnonymousMarker()]); - Assert.True(metadata.HasMetadata()); - } - - [Fact(Timeout = 5000)] - public void HasMetadata_should_return_false_when_absent() - { - var metadata = new TurboEndpointMetadata([]); - Assert.False(metadata.HasMetadata()); - } - - [Fact(Timeout = 5000)] - public void GetOrderedMetadata_should_return_all_matching_in_order() - { - var auth1 = new AuthorizeData("Policy1", null, null); - var auth2 = new AuthorizeData("Policy2", null, null); - var other = new TagsMetadata(["tag1"]); - var metadata = new TurboEndpointMetadata([auth1, other, auth2]); - - var result = metadata.GetOrderedMetadata().ToList(); - Assert.Equal(2, result.Count); - Assert.Equal("Policy1", result[0].Policy); - Assert.Equal("Policy2", result[1].Policy); - } - - [Fact(Timeout = 5000)] - public void Items_should_expose_all_metadata_objects() - { - var items = new object[] { new AuthorizeData("P", null, null), new TagsMetadata(["t"]) }; - var metadata = new TurboEndpointMetadata(items); - Assert.Equal(2, metadata.Items.Count); - } - - [Fact(Timeout = 5000)] - public void Merge_should_combine_group_and_route_metadata() - { - var group = new TurboEndpointMetadata([new AuthorizeData("GroupPolicy", null, null)]); - var route = new TurboEndpointMetadata([new TagsMetadata(["route-tag"])]); - - var merged = TurboEndpointMetadata.Merge(group, route); - Assert.True(merged.HasMetadata()); - Assert.True(merged.HasMetadata()); - Assert.Equal(2, merged.Items.Count); - } - - [Fact(Timeout = 5000)] - public void Merge_should_cumulate_authorize_data() - { - var group = new TurboEndpointMetadata([new AuthorizeData("P1", null, null)]); - var route = new TurboEndpointMetadata([new AuthorizeData("P2", null, null)]); - - var merged = TurboEndpointMetadata.Merge(group, route); - Assert.Equal(2, merged.GetOrderedMetadata().Count()); - } - - [Fact(Timeout = 5000)] - public void AllowAnonymous_should_coexist_with_RequireAuthorization() - { - var metadata = new TurboEndpointMetadata([ - new AuthorizeData("Policy", null, null), - new AllowAnonymousMarker() - ]); - - Assert.True(metadata.HasMetadata()); - Assert.True(metadata.HasMetadata()); - } - - private sealed record AllowAnonymousMarker : IAllowAnonymous; -} diff --git a/src/TurboHTTP.Tests/Server/ServerContextFactorySpec.cs b/src/TurboHTTP.Tests/Server/ServerContextFactorySpec.cs index 57bfd9c03..099cf1b60 100644 --- a/src/TurboHTTP.Tests/Server/ServerContextFactorySpec.cs +++ b/src/TurboHTTP.Tests/Server/ServerContextFactorySpec.cs @@ -1,48 +1,39 @@ using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Tests.Server; -public sealed class ServerContextFactorySpec +public sealed class FeatureCollectionFactorySpec { [Fact(Timeout = 5000)] public void Create_should_set_request_feature() { var requestFeature = new TurboHttpRequestFeature { Method = "POST", Path = "/api" }; - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - Assert.Equal("POST", ctx.Request.Method); - Assert.Equal("/api", ctx.Request.Path.Value); + var reqFeature = ctx.Get()!; + Assert.Equal("POST", reqFeature.Method); + Assert.Equal("/api", reqFeature.Path); } [Fact(Timeout = 5000)] public void Create_should_set_response_feature() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var responseFeature = ctx.Features.Get(); + var responseFeature = ctx.Get(); Assert.NotNull(responseFeature); } - [Fact(Timeout = 5000)] - public void Create_should_set_request_body_feature() - { - var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); - - var bodyFeature = ctx.Features.Get(); - Assert.NotNull(bodyFeature); - } - [Fact(Timeout = 5000)] public void Create_should_set_body_detection_true_when_has_body() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: true); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: true); - var detection = ctx.Features.Get(); + var detection = ctx.Get(); Assert.NotNull(detection); Assert.True(detection.CanHaveBody); } @@ -51,9 +42,9 @@ public void Create_should_set_body_detection_true_when_has_body() public void Create_should_set_body_detection_false_when_no_body() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var detection = ctx.Features.Get(); + var detection = ctx.Get(); Assert.NotNull(detection); Assert.False(detection.CanHaveBody); } @@ -62,12 +53,12 @@ public void Create_should_set_body_detection_false_when_no_body() public void Create_should_set_response_body_feature() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var responseBodyFeature = ctx.Features.Get(); + var responseBodyFeature = ctx.Get(); Assert.NotNull(responseBodyFeature); - var turboResponseBody = ctx.Features.Get(); + var turboResponseBody = ctx.Get(); Assert.NotNull(turboResponseBody); } @@ -75,55 +66,89 @@ public void Create_should_set_response_body_feature() public void Create_should_set_request_lifetime_feature() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var lifetime = ctx.Features.Get(); + var lifetime = ctx.Get(); Assert.NotNull(lifetime); - Assert.Equal(ctx.RequestAborted, lifetime.RequestAborted); } [Fact(Timeout = 5000)] - public void RequestLifetimeFeature_should_delegate_abort_to_context() + public void RequestLifetimeFeature_should_support_abort() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var lifetime = ctx.Features.Get()!; + var lifetime = ctx.Get()!; lifetime.Abort(); - Assert.True(ctx.RequestAborted.IsCancellationRequested); + Assert.True(lifetime.RequestAborted.IsCancellationRequested); } [Fact(Timeout = 5000)] public void Create_should_set_request_identifier_feature() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var identifier = ctx.Features.Get(); + var identifier = ctx.Get(); Assert.NotNull(identifier); - Assert.Equal(ctx.TraceIdentifier, identifier.TraceIdentifier); + Assert.NotEmpty(identifier.TraceIdentifier); } [Fact(Timeout = 5000)] - public void RequestIdentifierFeature_should_sync_with_context() + public void RequestIdentifierFeature_should_support_custom_trace_id() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var identifier = ctx.Features.Get()!; + var identifier = ctx.Get()!; identifier.TraceIdentifier = "custom-id"; - Assert.Equal("custom-id", ctx.TraceIdentifier); + Assert.Equal("custom-id", identifier.TraceIdentifier); } [Fact(Timeout = 5000)] public void Create_should_set_reset_feature_as_null_for_http11() { var requestFeature = new TurboHttpRequestFeature(); - var ctx = ServerContextFactory.Create(requestFeature, hasBody: false); + var ctx = FeatureCollectionFactory.Create(requestFeature, hasBody: false); - var reset = ctx.Features.Get(); + var reset = ctx.Get(); Assert.Null(reset); } -} + + [Fact(Timeout = 5000)] + public void Create_should_set_max_request_body_size_feature() + { + var requestFeature = new TurboHttpRequestFeature(); + var features = + FeatureCollectionFactory.Create(requestFeature, hasBody: false, maxRequestBodySize: 10 * 1024 * 1024); + + var maxBodyFeature = features.Get(); + Assert.NotNull(maxBodyFeature); + Assert.Equal(10 * 1024 * 1024, maxBodyFeature.MaxRequestBodySize); + Assert.False(maxBodyFeature.IsReadOnly); + } + + [Fact(Timeout = 5000)] + public void Create_should_set_null_max_body_size_when_unlimited() + { + var requestFeature = new TurboHttpRequestFeature(); + var features = FeatureCollectionFactory.Create(requestFeature, hasBody: false, maxRequestBodySize: null); + + var maxBodyFeature = features.Get(); + Assert.NotNull(maxBodyFeature); + Assert.Null(maxBodyFeature.MaxRequestBodySize); + } + + [Fact(Timeout = 5000)] + public void Create_should_set_body_control_feature_with_sync_io_disabled() + { + var requestFeature = new TurboHttpRequestFeature(); + var features = FeatureCollectionFactory.Create(requestFeature, hasBody: false); + + var bodyControl = features.Get(); + Assert.NotNull(bodyControl); + Assert.False(bodyControl.AllowSynchronousIO); + } +} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Server/TurboConnectionInfoSpec.cs b/src/TurboHTTP.Tests/Server/TurboConnectionInfoSpec.cs deleted file mode 100644 index 338bc7aa9..000000000 --- a/src/TurboHTTP.Tests/Server/TurboConnectionInfoSpec.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboConnectionInfoSpec -{ - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_store_connection_id() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Loopback, 443); - Assert.Equal("conn-1", info.Id); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_store_remote_endpoint() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Parse("192.168.1.1"), 54321, IPAddress.Loopback, 443); - Assert.Equal(IPAddress.Parse("192.168.1.1"), info.RemoteIpAddress); - Assert.Equal(54321, info.RemotePort); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_store_local_endpoint() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Parse("10.0.0.1"), 8080); - Assert.Equal(IPAddress.Parse("10.0.0.1"), info.LocalIpAddress); - Assert.Equal(8080, info.LocalPort); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_allow_null_addresses() - { - var info = new TurboConnectionInfo("conn-1", null, 0, null, 0); - Assert.Null(info.RemoteIpAddress); - Assert.Null(info.LocalIpAddress); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_support_late_binding_remote_endpoint() - { - var info = new TurboConnectionInfo("conn-1", null, 0, null, 8080); - - Assert.Null(info.RemoteIpAddress); - Assert.Equal(0, info.RemotePort); - - info.RemoteIpAddress = IPAddress.Parse("192.168.1.100"); - info.RemotePort = 54321; - - Assert.Equal(IPAddress.Parse("192.168.1.100"), info.RemoteIpAddress); - Assert.Equal(54321, info.RemotePort); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_be_assignable_to_ConnectionInfo() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Loopback, 443); - ConnectionInfo baseRef = info; - Assert.Equal("conn-1", baseRef.Id); - Assert.Equal(IPAddress.Loopback, baseRef.RemoteIpAddress); - Assert.Equal(12345, baseRef.RemotePort); - } - - [Fact(Timeout = 5000)] - public void TurboConnectionInfo_should_return_null_client_certificate() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Loopback, 443); - Assert.Null(info.ClientCertificate); - } - - [Fact(Timeout = 5000)] - public async Task TurboConnectionInfo_should_return_null_from_GetClientCertificateAsync() - { - var info = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Loopback, 443); - var cert = await info.GetClientCertificateAsync(TestContext.Current.CancellationToken); - Assert.Null(cert); - } -} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Server/TurboHttpContextSpec.cs b/src/TurboHTTP.Tests/Server/TurboHttpContextSpec.cs deleted file mode 100644 index 2c18185cd..000000000 --- a/src/TurboHTTP.Tests/Server/TurboHttpContextSpec.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboHttpContextSpec -{ - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_be_assignable_to_HttpContext() - { - var ctx = CreateContext(); - HttpContext baseRef = ctx; - Assert.NotNull(baseRef); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_request_as_HttpRequest() - { - var ctx = CreateContext(); - Assert.NotNull(ctx.Request); - Assert.Equal("GET", ctx.Request.Method); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_response_as_HttpResponse() - { - var ctx = CreateContext(); - Assert.NotNull(ctx.Response); - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_connection_as_ConnectionInfo() - { - var connection = new TurboConnectionInfo("conn-1", IPAddress.Loopback, 12345, IPAddress.Loopback, 443); - var ctx = ServerTestContext.Request() - .Get("/") - .Connection(connection) - .Build(); - Assert.Equal("conn-1", ctx.Connection.Id); - Assert.IsType(ctx.Connection); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_features() - { - var ctx = CreateContext(); - Assert.NotNull(ctx.Features); - Assert.NotNull(ctx.Features.Get()); - Assert.NotNull(ctx.Features.Get()); - Assert.NotNull(ctx.Features.Get()); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_empty_items() - { - var ctx = CreateContext(); - Assert.NotNull(ctx.Items); - Assert.Empty(ctx.Items); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_trace_identifier() - { - var ctx = CreateContext(); - Assert.NotNull(ctx.TraceIdentifier); - Assert.NotEmpty(ctx.TraceIdentifier); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_request_aborted() - { - var ctx = CreateContext(); - Assert.Equal(TestContext.Current.CancellationToken, ctx.RequestAborted); - } - - [Fact(Timeout = 5000)] - public void TurboHttpContext_should_expose_request_services() - { - var services = new ServiceCollection().BuildServiceProvider(); - var ctx = ServerTestContext.Request() - .Get("/") - .Services(services) - .RequestAborted(TestContext.Current.CancellationToken) - .Build(); - Assert.Same(services, ctx.RequestServices); - } - - private static TurboHttpContext CreateContext() - { - return ServerTestContext.Request() - .Get("/") - .Connection(new TurboConnectionInfo("test", IPAddress.Loopback, 0, IPAddress.Loopback, 0)) - .RequestAborted(TestContext.Current.CancellationToken) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboHttpResetFeatureSpec.cs b/src/TurboHTTP.Tests/Server/TurboHttpResetFeatureSpec.cs index 018e412f6..bc1800b46 100644 --- a/src/TurboHTTP.Tests/Server/TurboHttpResetFeatureSpec.cs +++ b/src/TurboHTTP.Tests/Server/TurboHttpResetFeatureSpec.cs @@ -1,4 +1,4 @@ -using TurboHTTP.Context.Features; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Tests.Server; diff --git a/src/TurboHTTP.Tests/Server/TurboKestrelConfigurationBinderSpec.cs b/src/TurboHTTP.Tests/Server/TurboKestrelConfigurationBinderSpec.cs deleted file mode 100644 index ed9783470..000000000 --- a/src/TurboHTTP.Tests/Server/TurboKestrelConfigurationBinderSpec.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Security.Authentication; -using Microsoft.Extensions.Configuration; -using TurboHTTP.Server; -using TurboHTTP.Server.Hosting; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboKestrelConfigurationBinderSpec -{ - [Fact(Timeout = 5000)] - public void Bind_should_parse_http_endpoint() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:Endpoints:Http:Url"] = "http://localhost:5000" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Contains("http://localhost:5000", options.Urls); - } - - [Fact(Timeout = 5000)] - public void Bind_should_parse_https_endpoint_with_certificate() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:Endpoints:Https:Url"] = "https://localhost:5001", - ["TurboKestrel:Endpoints:Https:Certificate:Path"] = "certs/server.pfx", - ["TurboKestrel:Endpoints:Https:Certificate:Password"] = "changeit" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Single(options.ListenOptions); - Assert.True(options.ListenOptions[0].IsHttps); - Assert.Equal("certs/server.pfx", options.ListenOptions[0].HttpsOptions!.CertificatePath); - Assert.Equal("changeit", options.ListenOptions[0].HttpsOptions!.CertificatePassword); - } - - [Fact(Timeout = 5000)] - public void Bind_should_parse_ssl_protocols() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:Endpoints:Https:Url"] = "https://localhost:5001", - ["TurboKestrel:Endpoints:Https:Certificate:Path"] = "cert.pfx", - ["TurboKestrel:Endpoints:Https:SslProtocols"] = "Tls12, Tls13" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Equal(SslProtocols.Tls12 | SslProtocols.Tls13, options.ListenOptions[0].HttpsOptions!.EnabledSslProtocols); - } - - [Fact(Timeout = 5000)] - public void Bind_should_parse_http_protocols() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:Endpoints:Api:Url"] = "http://localhost:5000", - ["TurboKestrel:Endpoints:Api:Protocols"] = "Http2" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Single(options.ListenOptions); - Assert.Equal(HttpProtocols.Http2, options.ListenOptions[0].Protocols); - } - - [Fact(Timeout = 5000)] - public void Bind_should_parse_https_defaults() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:HttpsDefaults:SslProtocols"] = "Tls13", - ["TurboKestrel:HttpsDefaults:HandshakeTimeout"] = "00:00:30" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.NotNull(options.HttpsDefaultsCallback); - - var httpsOptions = new TurboHttpsOptions(); - options.HttpsDefaultsCallback!(httpsOptions); - Assert.Equal(SslProtocols.Tls13, httpsOptions.EnabledSslProtocols); - Assert.Equal(TimeSpan.FromSeconds(30), httpsOptions.HandshakeTimeout); - } - - [Fact(Timeout = 5000)] - public void Bind_should_parse_multiple_endpoints() - { - var config = BuildConfig(new Dictionary - { - ["TurboKestrel:Endpoints:Http:Url"] = "http://localhost:5000", - ["TurboKestrel:Endpoints:Api:Url"] = "http://localhost:6000" - }); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Equal(2, options.Urls.Count); - } - - [Fact(Timeout = 5000)] - public void Bind_should_ignore_missing_section() - { - var config = BuildConfig(new Dictionary()); - - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, config.GetSection("TurboKestrel")); - - Assert.Empty(options.Urls); - Assert.Empty(options.ListenOptions); - } - - private static IConfiguration BuildConfig(Dictionary values) - { - return new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build(); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboMiddlewareExtensionsSpec.cs b/src/TurboHTTP.Tests/Server/TurboMiddlewareExtensionsSpec.cs deleted file mode 100644 index 3308633a5..000000000 --- a/src/TurboHTTP.Tests/Server/TurboMiddlewareExtensionsSpec.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboMiddlewareExtensionsSpec -{ - [Fact(Timeout = 5000)] - public async Task UseTurbo_should_register_middleware() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var called = false; - app.UseTurbo(async (ctx, next) => { called = true; await next(ctx); }); - - var pipeline = app.Services.GetRequiredService().Build(); - await pipeline(ServerTestContext.Request().Get("/test").Build()); - - Assert.True(called); - } - - [Fact(Timeout = 5000)] - public async Task MapTurboWhen_should_register_conditional_middleware() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var called = false; - app.MapTurboWhen( - _ => true, - branch => branch.Use((ctx, next) => { called = true; return next(ctx); })); - - var pipeline = app.Services.GetRequiredService().Build(); - await pipeline(ServerTestContext.Request().Get("/test").Build()); - - Assert.True(called); - } - - [Fact(Timeout = 5000)] - public async Task MapTurbo_should_register_path_branch() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var called = false; - app.MapTurbo("/admin", branch => - { - branch.Use((ctx, next) => { called = true; return next(ctx); }); - }); - - var pipeline = app.Services.GetRequiredService().Build(); - var ctx = ServerTestContext.Request().Get("/admin/dashboard").Build(); - await pipeline(ctx); - - Assert.True(called); - } - - [Fact(Timeout = 5000)] - public void AddTurboKestrel_should_register_pipeline_builder() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - Assert.NotNull(app.Services.GetRequiredService()); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboPipelineBuilderSpec.cs b/src/TurboHTTP.Tests/Server/TurboPipelineBuilderSpec.cs deleted file mode 100644 index c74789e6a..000000000 --- a/src/TurboHTTP.Tests/Server/TurboPipelineBuilderSpec.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboPipelineBuilderSpec -{ - private static TurboHttpContext CreateTestContext() - { - return ServerTestContext.Request() - .Get("/test") - .Services(new ServiceCollection().BuildServiceProvider()) - .Build(); - } - - [Fact(Timeout = 5000)] - public async Task Build_should_return_noop_when_empty() - { - var builder = new TurboPipelineBuilder(); - var pipeline = builder.Build(); - - var ctx = CreateTestContext(); - await pipeline(ctx); - - Assert.Equal(200, ctx.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Build_should_execute_middleware_in_order() - { - var order = new List(); - var builder = new TurboPipelineBuilder(); - builder.Use((ctx, next) => { order.Add(1); return next(ctx); }); - builder.Use((ctx, next) => { order.Add(2); return next(ctx); }); - - var pipeline = builder.Build(); - await pipeline(CreateTestContext()); - - Assert.Equal([1, 2], order); - } - - [Fact(Timeout = 5000)] - public async Task Build_should_allow_Run_as_terminal() - { - var terminated = false; - var builder = new TurboPipelineBuilder(); - builder.Run(_ => { terminated = true; return Task.CompletedTask; }); - - var pipeline = builder.Build(); - await pipeline(CreateTestContext()); - - Assert.True(terminated); - } - - [Fact(Timeout = 5000)] - public async Task Build_should_support_Map_branching() - { - var branched = false; - var builder = new TurboPipelineBuilder(); - builder.Map("/admin", branch => - { - branch.Use((ctx, next) => { branched = true; return next(ctx); }); - }); - - var pipeline = builder.Build(); - - var ctx = ServerTestContext.Request() - .Get("/admin/dashboard") - .Services(new ServiceCollection().BuildServiceProvider()) - .Build(); - - await pipeline(ctx); - - Assert.True(branched); - } - - [Fact(Timeout = 5000)] - public async Task Build_should_support_MapWhen_predicate() - { - var branched = false; - var builder = new TurboPipelineBuilder(); - builder.MapWhen(_ => true, branch => - { - branch.Use((ctx, next) => { branched = true; return next(ctx); }); - }); - - var pipeline = builder.Build(); - await pipeline(CreateTestContext()); - - Assert.True(branched); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboRoutingExtensionsSpec.cs b/src/TurboHTTP.Tests/Server/TurboRoutingExtensionsSpec.cs deleted file mode 100644 index d89faf588..000000000 --- a/src/TurboHTTP.Tests/Server/TurboRoutingExtensionsSpec.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboRoutingExtensionsSpec -{ - [Fact(Timeout = 5000)] - public void MapTurboGet_should_register_route() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var handlerBuilder = app.MapTurboGet("/health", () => TypedResults.Ok()); - Assert.IsType(handlerBuilder); - - var table = app.Services.GetRequiredService(); - var frozen = table.Freeze(); - Assert.True(frozen.Match("GET", "/health").IsMatch); - } - - [Fact(Timeout = 5000)] - public void MapTurboPost_should_register_route() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - app.MapTurboPost("/items", () => TypedResults.Created("/items/1", new { Id = 1 })); - - var table = app.Services.GetRequiredService(); - var frozen = table.Freeze(); - Assert.True(frozen.Match("POST", "/items").IsMatch); - } - - [Fact(Timeout = 5000)] - public void MapTurboGroup_should_return_group_builder() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var group = app.MapTurboGroup("/api"); - Assert.IsType(group); - } - - [Fact(Timeout = 5000)] - public void MapTurboGroup_routes_should_resolve() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - app.MapTurboGroup("/api") - .MapGet("/users", () => TypedResults.Ok()); - - var table = app.Services.GetRequiredService(); - var frozen = table.Freeze(); - Assert.True(frozen.Match("GET", "/api/users").IsMatch); - } - - [Fact(Timeout = 5000)] - public void MapTurboGet_should_support_fluent_metadata() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - - var handlerBuilder = app.MapTurboGet("/test", () => TypedResults.Ok()) - .WithName("GetTest") - .WithTags("test"); - - Assert.Equal("GetTest", handlerBuilder.Metadata.Name); - Assert.Contains("test", handlerBuilder.Metadata.Tags); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboServerHostingSpec.cs b/src/TurboHTTP.Tests/Server/TurboServerHostingSpec.cs deleted file mode 100644 index 9de8be3b3..000000000 --- a/src/TurboHTTP.Tests/Server/TurboServerHostingSpec.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; -using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboServerHostingSpec -{ - [Fact(Timeout = 5000)] - public void AddTurboKestrel_should_register_options() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(opts => opts.GracefulShutdownTimeout = TimeSpan.FromSeconds(60)); - var app = builder.Build(); - var options = app.Services.GetRequiredService(); - Assert.Equal(TimeSpan.FromSeconds(60), options.GracefulShutdownTimeout); - } - - [Fact(Timeout = 5000)] - public void AddTurboKestrel_should_register_route_table() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - Assert.NotNull(app.Services.GetRequiredService()); - } - - [Fact(Timeout = 5000)] - public void AddTurboKestrel_should_register_pipeline_builder() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - Assert.NotNull(app.Services.GetRequiredService()); - } - - [Fact(Timeout = 5000)] - public void AddTurboKestrel_should_not_register_separate_entity_route_table() - { - var builder = WebApplication.CreateBuilder(); - builder.Services.AddTurboKestrel(); - var app = builder.Build(); - Assert.NotNull(app.Services.GetRequiredService()); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboServerLimitsSpec.cs b/src/TurboHTTP.Tests/Server/TurboServerLimitsSpec.cs deleted file mode 100644 index 34d8babec..000000000 --- a/src/TurboHTTP.Tests/Server/TurboServerLimitsSpec.cs +++ /dev/null @@ -1,137 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboServerLimitsSpec -{ - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_max_request_body_size_to_30MB() - { - var limits = new TurboServerLimits(); - Assert.Equal(30 * 1024 * 1024, limits.MaxRequestBodySize); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_max_request_header_count_to_100() - { - var limits = new TurboServerLimits(); - Assert.Equal(100, limits.MaxRequestHeaderCount); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_max_request_headers_total_size_to_32KB() - { - var limits = new TurboServerLimits(); - Assert.Equal(32 * 1024, limits.MaxRequestHeadersTotalSize); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_keep_alive_timeout_to_130_seconds() - { - var limits = new TurboServerLimits(); - Assert.Equal(TimeSpan.FromSeconds(130), limits.KeepAliveTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_request_headers_timeout_to_30_seconds() - { - var limits = new TurboServerLimits(); - Assert.Equal(TimeSpan.FromSeconds(30), limits.RequestHeadersTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_min_request_body_data_rate_grace_period_to_5_seconds() - { - var limits = new TurboServerLimits(); - Assert.Equal(TimeSpan.FromSeconds(5), limits.MinRequestBodyDataRateGracePeriod); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_default_min_response_data_rate_grace_period_to_5_seconds() - { - var limits = new TurboServerLimits(); - Assert.Equal(TimeSpan.FromSeconds(5), limits.MinResponseDataRateGracePeriod); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_allow_max_concurrent_connections_to_be_set() - { - var limits = new TurboServerLimits { MaxConcurrentConnections = 1000 }; - Assert.Equal(1000, limits.MaxConcurrentConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerLimits_should_allow_max_concurrent_upgraded_connections_to_be_set() - { - var limits = new TurboServerLimits { MaxConcurrentUpgradedConnections = 500 }; - Assert.Equal(500, limits.MaxConcurrentUpgradedConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_Limits_should_delegate_MaxConcurrentConnections_to_deprecated_property() - { - var options = new TurboServerOptions(); - options.MaxConcurrentConnections = 1000; - Assert.Equal(1000, options.Limits.MaxConcurrentConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_deprecated_MaxConcurrentConnections_should_delegate_to_Limits() - { - var options = new TurboServerOptions(); - options.Limits.MaxConcurrentConnections = 2000; - Assert.Equal(2000, options.MaxConcurrentConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_Limits_should_delegate_MaxConcurrentUpgradedConnections_to_deprecated_property() - { - var options = new TurboServerOptions(); - options.MaxConcurrentUpgradedConnections = 500; - Assert.Equal(500, options.Limits.MaxConcurrentUpgradedConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_deprecated_MaxConcurrentUpgradedConnections_should_delegate_to_Limits() - { - var options = new TurboServerOptions(); - options.Limits.MaxConcurrentUpgradedConnections = 300; - Assert.Equal(300, options.MaxConcurrentUpgradedConnections); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_Limits_should_delegate_KeepAliveTimeout_to_deprecated_property() - { - var options = new TurboServerOptions(); - var timeout = TimeSpan.FromSeconds(60); - options.KeepAliveTimeout = timeout; - Assert.Equal(timeout, options.Limits.KeepAliveTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_deprecated_KeepAliveTimeout_should_delegate_to_Limits() - { - var options = new TurboServerOptions(); - var timeout = TimeSpan.FromSeconds(90); - options.Limits.KeepAliveTimeout = timeout; - Assert.Equal(timeout, options.KeepAliveTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_Limits_should_delegate_RequestHeadersTimeout_to_deprecated_property() - { - var options = new TurboServerOptions(); - var timeout = TimeSpan.FromSeconds(15); - options.RequestHeadersTimeout = timeout; - Assert.Equal(timeout, options.Limits.RequestHeadersTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_deprecated_RequestHeadersTimeout_should_delegate_to_Limits() - { - var options = new TurboServerOptions(); - var timeout = TimeSpan.FromSeconds(45); - options.Limits.RequestHeadersTimeout = timeout; - Assert.Equal(timeout, options.RequestHeadersTimeout); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboServerOptionsBindingSpec.cs b/src/TurboHTTP.Tests/Server/TurboServerOptionsBindingSpec.cs deleted file mode 100644 index 1c178148a..000000000 --- a/src/TurboHTTP.Tests/Server/TurboServerOptionsBindingSpec.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Net; -using System.Security.Cryptography.X509Certificates; -using TurboHTTP.Server; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboServerOptionsBindingSpec -{ - [Fact(Timeout = 5000)] - public void Urls_should_be_empty_by_default() - { - var options = new TurboServerOptions(); - Assert.Empty(options.Urls); - } - - [Fact(Timeout = 5000)] - public void Urls_should_accept_url_strings() - { - var options = new TurboServerOptions(); - options.Urls.Add("http://localhost:5000"); - options.Urls.Add("https://localhost:5001"); - Assert.Equal(2, options.Urls.Count); - } - - [Fact(Timeout = 5000)] - public void Listen_should_add_listen_options_with_address_and_port() - { - var options = new TurboServerOptions(); - options.Listen(IPAddress.Loopback, 5000); - Assert.Single(options.ListenOptions); - Assert.Equal(IPAddress.Loopback, options.ListenOptions[0].Address); - Assert.Equal((ushort)5000, options.ListenOptions[0].Port); - } - - [Fact(Timeout = 5000)] - public void Listen_with_configure_should_apply_callback() - { - var options = new TurboServerOptions(); - options.Listen(IPAddress.Any, 443, listen => - { - listen.Protocols = HttpProtocols.Http2; - }); - Assert.Equal(HttpProtocols.Http2, options.ListenOptions[0].Protocols); - } - - [Fact(Timeout = 5000)] - public void ListenLocalhost_should_use_loopback_address() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(8080); - Assert.Equal(IPAddress.Loopback, options.ListenOptions[0].Address); - Assert.Equal((ushort)8080, options.ListenOptions[0].Port); - } - - [Fact(Timeout = 5000)] - public void ListenLocalhost_with_configure_should_apply_callback() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(443, listen => - { - listen.UseHttps(); - }); - Assert.True(options.ListenOptions[0].IsHttps); - } - - [Fact(Timeout = 5000)] - public void ListenAnyIP_should_use_any_address() - { - var options = new TurboServerOptions(); - options.ListenAnyIP(80); - Assert.Equal(IPAddress.Any, options.ListenOptions[0].Address); - Assert.Equal((ushort)80, options.ListenOptions[0].Port); - } - - [Fact(Timeout = 5000)] - public void ListenAnyIP_with_configure_should_apply_callback() - { - using var cert = CreateSelfSignedCert(); - var options = new TurboServerOptions(); - options.ListenAnyIP(443, listen => - { - listen.UseHttps(cert); - }); - Assert.True(options.ListenOptions[0].IsHttps); - Assert.Same(cert, options.ListenOptions[0].HttpsOptions!.ServerCertificate); - } - - [Fact(Timeout = 5000)] - public void ConfigureHttpsDefaults_should_store_defaults_callback() - { - var options = new TurboServerOptions(); - options.ConfigureHttpsDefaults(https => - { - https.CertificatePath = "default.pfx"; - }); - Assert.NotNull(options.HttpsDefaultsCallback); - } - - [Fact(Timeout = 5000)] - public void Multiple_listen_calls_should_accumulate() - { - var options = new TurboServerOptions(); - options.ListenLocalhost(5000); - options.ListenLocalhost(5001); - options.ListenAnyIP(8080); - Assert.Equal(3, options.ListenOptions.Count); - } - - private static X509Certificate2 CreateSelfSignedCert() - { - using var rsa = System.Security.Cryptography.RSA.Create(2048); - var request = new CertificateRequest("CN=Test", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); - return request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboServerOptionsSpec.cs b/src/TurboHTTP.Tests/Server/TurboServerOptionsSpec.cs deleted file mode 100644 index dbaba4d5f..000000000 --- a/src/TurboHTTP.Tests/Server/TurboServerOptionsSpec.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Servus.Akka.Transport; -using Servus.Akka.Transport.Tcp.Listener; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboServerOptionsSpec -{ - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_keep_alive_to_130_seconds() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(TimeSpan.FromSeconds(130), options.KeepAliveTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_request_headers_timeout_to_30_seconds() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(TimeSpan.FromSeconds(30), options.RequestHeadersTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_graceful_shutdown_to_30_seconds() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(TimeSpan.FromSeconds(30), options.GracefulShutdownTimeout); - } - - [Fact(Timeout = 5000)] - public void Http2ServerOptions_should_default_max_concurrent_streams_to_100() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(100, options.Http2.MaxConcurrentStreams); - } - - [Fact(Timeout = 5000)] - public void Http2ServerOptions_should_default_max_frame_size_to_16384() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(16384, options.Http2.MaxFrameSize); - } - - [Fact(Timeout = 5000)] - public void Http3ServerOptions_should_default_max_concurrent_streams_to_100() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(100, options.Http3.MaxConcurrentStreams); - } - - [Fact(Timeout = 5000)] - public void Http3ServerOptions_should_default_web_transport_to_disabled() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.False(options.Http3.EnableWebTransport); - } - - [Fact(Timeout = 5000)] - public void Endpoints_should_be_empty_by_default() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - - Assert.Empty(options.Endpoints); - } - - [Fact(Timeout = 5000)] - public void Endpoints_should_accept_listener_bindings() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - options.Endpoints.Add(new TurboHTTP.Server.ListenerBinding - { - Options = new TcpListenerOptions { Host = "0.0.0.0", Port = 8080 }, - Factory = new TcpListenerFactory() - }); - - Assert.Single(options.Endpoints); - Assert.Equal(8080, ((TcpListenerOptions)options.Endpoints[0].Options).Port); - } - - [Fact(Timeout = 5000)] - public void Bind_with_TcpListenerOptions_should_add_endpoint_with_TcpListenerFactory() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - options.Bind(new TcpListenerOptions { Host = "0.0.0.0", Port = 8080 }); - Assert.Single(options.Endpoints); - Assert.IsType(options.Endpoints[0].Factory); - } - - [Fact(Timeout = 5000)] - public void Bind_with_custom_factory_should_add_endpoint() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - var factory = new TcpListenerFactory(); - options.Bind(new TcpListenerOptions { Host = "0.0.0.0", Port = 9090 }, factory); - Assert.Single(options.Endpoints); - Assert.Same(factory, options.Endpoints[0].Factory); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_body_buffer_threshold_to_65536() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(65536, options.BodyBufferThreshold); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_body_consumption_timeout_to_30_seconds() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(TimeSpan.FromSeconds(30), options.BodyConsumptionTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboServerOptions_should_default_response_body_chunk_size_to_16384() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(16384, options.ResponseBodyChunkSize); - } - - [Fact(Timeout = 5000)] - public void Http2ServerOptions_should_default_initial_connection_window_to_1MB() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(1 * 1024 * 1024, options.Http2.InitialConnectionWindowSize); - } - - [Fact(Timeout = 5000)] - public void Http2ServerOptions_should_default_initial_stream_window_to_768KB() - { - var options = new TurboHTTP.Server.TurboServerOptions(); - Assert.Equal(768 * 1024, options.Http2.InitialStreamWindowSize); - } -} \ No newline at end of file diff --git a/src/TurboHTTP.Tests/Server/TurboStreamResultsSpec.cs b/src/TurboHTTP.Tests/Server/TurboStreamResultsSpec.cs deleted file mode 100644 index ddf30e435..000000000 --- a/src/TurboHTTP.Tests/Server/TurboStreamResultsSpec.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Text; -using Akka.Actor; -using Akka.Streams; -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Server; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboStreamResultsSpec : IDisposable -{ - private readonly ActorSystem _system; - private readonly IMaterializer _materializer; - - public TurboStreamResultsSpec() - { - _system = ActorSystem.Create("test"); - _materializer = _system.Materializer(); - } - - [Fact(Timeout = 5000)] - public void EventStream_should_return_IResult() - { - var source = Source.Single("hello"); - var result = TurboStreamResults.EventStream(source); - Assert.IsAssignableFrom(result); - } - - [Fact(Timeout = 5000)] - public void Stream_should_return_IResult() - { - var source = Source.Single(new ReadOnlyMemory("Hello"u8.ToArray())); - var result = TurboStreamResults.Stream(source); - Assert.IsAssignableFrom(result); - } - - [Fact(Timeout = 5000)] - public void Stream_with_content_type_should_return_IResult() - { - var source = Source.Single(new ReadOnlyMemory("Hello"u8.ToArray())); - var result = TurboStreamResults.Stream(source, "application/json"); - Assert.IsAssignableFrom(result); - } - - [Fact(Timeout = 5000)] - public async Task AkkaStreamResult_should_materialize_source_into_pipe_writer() - { - var ctx = CreateTestContext(); - var source = Source.From([ - new ReadOnlyMemory("chunk1"u8.ToArray()), - new ReadOnlyMemory("chunk2"u8.ToArray()) - ]); - - var result = TurboStreamResults.Stream(source, "application/octet-stream"); - await result.ExecuteAsync(ctx); - - var bodyFeature = ctx.Features.Get() as TurboHttpResponseBodyFeature; - var chunks = await bodyFeature!.GetResponseSource() - .RunWith(Sink.Seq>(), _materializer); - - var body = string.Concat(chunks.Select(c => Encoding.UTF8.GetString(c.Span))); - Assert.Equal("chunk1chunk2", body); - Assert.Equal(200, ctx.Response.StatusCode); - Assert.Equal("application/octet-stream", ctx.Response.ContentType); - } - - [Fact(Timeout = 5000)] - public async Task EventStreamResult_should_format_as_sse_and_materialize() - { - var ctx = CreateTestContext(); - var source = Source.From(["event1", "event2"]); - - var result = TurboStreamResults.EventStream(source); - await result.ExecuteAsync(ctx); - - var bodyFeature = ctx.Features.Get() as TurboHttpResponseBodyFeature; - var chunks = await bodyFeature!.GetResponseSource() - .RunWith(Sink.Seq>(), _materializer); - - var body = string.Concat(chunks.Select(c => Encoding.UTF8.GetString(c.Span))); - Assert.Contains("data: event1\n\n", body); - Assert.Contains("data: event2\n\n", body); - Assert.Equal("text/event-stream", ctx.Response.ContentType); - } - - private TurboHttpContext CreateTestContext() - { - var features = new TurboFeatureCollection(); - var responseFeature = new TurboHttpResponseFeature(); - features.Set(responseFeature); - var bodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(bodyFeature); - features.Set(bodyFeature); - - return new TurboHttpContext( - features, - new TurboConnectionInfo("test", null, 0, null, 0), - new FakeServiceProvider(), - CancellationToken.None, null!) - { - Materializer = _materializer - }; - } - - public void Dispose() - { - _system.Dispose(); - } - - private sealed class FakeServiceProvider : IServiceProvider - { - public object? GetService(Type serviceType) => null; - } -} diff --git a/src/TurboHTTP.Tests/Server/TurboWebApplicationSpec.cs b/src/TurboHTTP.Tests/Server/TurboWebApplicationSpec.cs deleted file mode 100644 index 075a9904b..000000000 --- a/src/TurboHTTP.Tests/Server/TurboWebApplicationSpec.cs +++ /dev/null @@ -1,276 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using TurboHTTP.Routing; -using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Tests.Server; - -public sealed class TurboWebApplicationSpec -{ - [Fact(Timeout = 5000)] - public void AddTurboKestrel_with_instance_should_register_same_instance() - { - var options = new TurboServerOptions - { - HandlerTimeout = TimeSpan.FromSeconds(99) - }; - - var builder = Host.CreateApplicationBuilder(); - builder.Services.AddTurboKestrel(options); - var host = builder.Build(); - - var resolved = host.Services.GetRequiredService(); - Assert.Same(options, resolved); - Assert.Equal(TimeSpan.FromSeconds(99), resolved.HandlerTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboUrlCollection_Add_should_delegate_to_options_urls() - { - var options = new TurboServerOptions(); - var urls = new TurboUrlCollection(options) - { - "http://localhost:5000", - "https://localhost:5001" - }; - - Assert.Equal(2, urls.Count); - Assert.Contains("http://localhost:5000", options.Urls); - Assert.Contains("https://localhost:5001", options.Urls); - } - - [Fact(Timeout = 5000)] - public void TurboUrlCollection_should_implement_ICollection() - { - var options = new TurboServerOptions(); - var urls = new TurboUrlCollection(options) { "http://localhost:5000" }; - - Assert.False(urls.IsReadOnly); - Assert.Contains("http://localhost:5000", urls); - Assert.DoesNotContain("http://localhost:9999", urls); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_should_expose_services() - { - var builder = new TurboWebApplicationBuilder(null); - - Assert.NotNull(builder.Services); - Assert.NotNull(builder.Configuration); - Assert.NotNull(builder.Logging); - Assert.NotNull(builder.Environment); - Assert.NotNull(builder.Server); - Assert.NotNull(builder.Host); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Build_should_return_app_with_registered_services() - { - var builder = new TurboWebApplicationBuilder(null); - var app = builder.Build(); - - Assert.NotNull(app); - Assert.NotNull(app.Services); - Assert.NotNull(app.Services.GetService()); - Assert.NotNull(app.Services.GetService()); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Server_should_be_same_instance_in_app() - { - var builder = new TurboWebApplicationBuilder(null) - { - Server = - { - HandlerTimeout = TimeSpan.FromSeconds(99) - } - }; - var app = builder.Build(); - - var resolved = app.Services.GetRequiredService(); - Assert.Same(builder.Server, resolved); - Assert.Equal(TimeSpan.FromSeconds(99), resolved.HandlerTimeout); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Host_ConfigureHostOptions_should_configure_host_options() - { - var builder = new TurboWebApplicationBuilder(null); - - var result = builder.Host.ConfigureHostOptions(options => - { - options.ShutdownTimeout = TimeSpan.FromSeconds(15); - }); - - Assert.NotNull(result); - var app = builder.Build(); - Assert.NotNull(app); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Host_ConfigureAppConfiguration_should_add_configuration() - { - var builder = new TurboWebApplicationBuilder(null); - - builder.Host.ConfigureAppConfiguration(config => - { - var data = new Dictionary { { "test-key", "test-value" } }; - config.AddInMemoryCollection(data); - }); - - var configValue = builder.Configuration["test-key"]; - Assert.Equal("test-value", configValue); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Host_ConfigureServices_should_register_service() - { - var builder = new TurboWebApplicationBuilder(null); - - builder.Host.ConfigureServices(services => - { - services.AddScoped(); - }); - - var app = builder.Build(); - var service = app.Services.GetService(); - Assert.NotNull(service); - } - - [Fact(Timeout = 5000)] - public void TurboWebApplicationBuilder_Host_methods_should_return_self_for_chaining() - { - var builder = new TurboWebApplicationBuilder(null); - var host = builder.Host; - - var result1 = host.ConfigureHostOptions(_ => { }); - var result2 = result1.ConfigureAppConfiguration(_ => { }); - var result3 = result2.ConfigureServices(_ => { }); - - Assert.Same(host, result1); - Assert.Same(host, result2); - Assert.Same(host, result3); - } - - [Fact(Timeout = 5000)] - public void CreateBuilder_should_return_builder() - { - var builder = TurboWebApplication.CreateBuilder(); - Assert.NotNull(builder); - Assert.NotNull(builder.Services); - } - - [Fact(Timeout = 5000)] - public void CreateBuilder_with_args_should_return_builder() - { - var builder = TurboWebApplication.CreateBuilder(["--environment", "Development"]); - Assert.NotNull(builder); - } - - [Fact(Timeout = 5000)] - public void Create_should_build_app_with_defaults() - { - var app = TurboWebApplication.Create(); - Assert.NotNull(app); - Assert.NotNull(app.Services); - Assert.NotNull(app.Configuration); - Assert.NotNull(app.Environment); - } - - [Fact(Timeout = 5000)] - public void App_Urls_should_delegate_to_server_options() - { - var app = TurboWebApplication.Create(); - app.Urls.Add("http://localhost:5000"); - - var options = app.Services.GetRequiredService(); - Assert.Contains("http://localhost:5000", options.Urls); - } - - [Fact(Timeout = 5000)] - public void App_Logger_should_be_available() - { - var app = TurboWebApplication.Create(); - Assert.NotNull(app.Logger); - } - - [Fact(Timeout = 5000)] - public void MapGet_extension_should_register_route() - { - var app = TurboWebApplication.Create(); - var result = app.MapGet("/test", () => Results.Ok()); - Assert.NotNull(result); - } - - [Fact(Timeout = 5000)] - public void MapPost_extension_should_register_route() - { - var app = TurboWebApplication.Create(); - var result = app.MapPost("/test", () => Results.Ok()); - Assert.NotNull(result); - } - - [Fact(Timeout = 5000)] - public void MapGroup_extension_should_return_group_builder() - { - var app = TurboWebApplication.Create(); - var group = app.MapGroup("/api"); - Assert.NotNull(group); - } - - [Fact(Timeout = 5000)] - public void MapGet_with_context_handler_should_register_route() - { - var app = TurboWebApplication.Create(); - var result = app.MapGet("/test", _ => Task.CompletedTask); - Assert.NotNull(result); - } - - [Fact(Timeout = 5000)] - public void Use_should_return_app_for_chaining() - { - var app = TurboWebApplication.Create(); - - var result = app.Use(async (ctx, next) => await next(ctx)); - - Assert.Same(app, result); - } - - [Fact(Timeout = 5000)] - public void Run_should_return_app_for_chaining() - { - var app = TurboWebApplication.Create(); - - var result = app.Run(_ => Task.CompletedTask); - - Assert.Same(app, result); - } - - [Fact(Timeout = 5000)] - public void Map_should_return_app_for_chaining() - { - var app = TurboWebApplication.Create(); - - var result = app.Map("/branch", branch => branch.Run(_ => Task.CompletedTask)); - - Assert.Same(app, result); - } - - [Fact(Timeout = 5000)] - public void Pipeline_interface_should_also_work() - { - var app = TurboWebApplication.Create(); - ITurboApplicationBuilder pipeline = app; - - var result = pipeline.Use(async (ctx, next) => await next(ctx)); - - Assert.Same(app, result); - } -} - -internal sealed class TestService -{ -} diff --git a/src/TurboHTTP.Tests/Streams/Stages/Lifecycle/ListenerActorConnectionLimitSpec.cs b/src/TurboHTTP.Tests/Streams/Stages/Lifecycle/ListenerActorConnectionLimitSpec.cs deleted file mode 100644 index 2e7598468..000000000 --- a/src/TurboHTTP.Tests/Streams/Stages/Lifecycle/ListenerActorConnectionLimitSpec.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Akka; -using Akka.Actor; -using Akka.Streams; -using Akka.Streams.Dsl; -using Akka.TestKit.Xunit; -using Microsoft.Extensions.DependencyInjection; -using Servus.Akka.Transport; -using TurboHTTP.Server; -using TurboHTTP.Streams.Lifecycle; - -namespace TurboHTTP.Tests.Streams.Stages.Lifecycle; - -public sealed class ListenerActorConnectionLimitSpec : TestKit -{ - private sealed class FakeListenerFactory : IListenerFactory - { - private readonly Source, NotUsed> _source; - - public FakeListenerFactory(Source, NotUsed> source) - { - _source = source; - } - - public Source, Task> Bind(ListenerOptions options) - { - return _source.MapMaterializedValue(_ => Task.CompletedTask); - } - } - - private sealed class ParentForListener : ReceiveActor - { - private IActorRef? _testActor; - - public ParentForListener() - { - Receive(msg => - { - _testActor = Sender; - - var factory = new FakeListenerFactory( - Source.Empty>()); - var serverOptions = new TurboServerOptions - { - MaxConcurrentConnections = msg.MaxConcurrentConnections - }; - TurboRequestDelegate pipeline = _ => Task.CompletedTask; - var routeTable = new TurboHTTP.Routing.RouteTable([]); - var services = new ServiceCollection().BuildServiceProvider(); - var materializer = Context.System.Materializer(); - - var listenerActor = Context.ActorOf( - ListenerActor.Create( - factory, - msg.ListenerOptions, - serverOptions, - pipeline, - routeTable, - services, - materializer), - "listener"); - - Context.Watch(listenerActor); - _testActor.Tell(listenerActor, ActorRefs.NoSender); - }); - - Receive(msg => - { - _testActor?.Tell(msg, ActorRefs.NoSender); - }); - } - - public sealed record CreateListenerWithLimit( - ListenerOptions ListenerOptions, - int MaxConcurrentConnections); - } - - private static Flow CreateDummyConnectionFlow() - { - return Flow.Create() - .Select(_ => (ITransportInbound)new TransportData(TransportBuffer.Rent(1))); - } - - [Fact(Timeout = 5000)] - public void ListenerActor_should_accept_connections_when_limit_is_zero() - { - var listenerOptions = new TcpListenerOptions { Host = "localhost", Port = 8080 }; - - var parentActor = Sys.ActorOf(Props.Create(() => new ParentForListener()), "parent-unlimited"); - - parentActor.Tell( - new ParentForListener.CreateListenerWithLimit( - listenerOptions, - MaxConcurrentConnections: 0), - TestActor); - - var listenerActor = ExpectMsg(cancellationToken: TestContext.Current.CancellationToken); - - // Send multiple connections - for (var i = 0; i < 5; i++) - { - var dummyFlow = CreateDummyConnectionFlow(); - listenerActor.Tell(new ListenerActor.IncomingConnection(dummyFlow), ActorRefs.NoSender); - } - - // All connections should be accepted (ConnectionStarted messages received) - for (var i = 0; i < 5; i++) - { - ExpectMsg( - cancellationToken: TestContext.Current.CancellationToken); - } - } - - [Fact(Timeout = 5000)] - public void ListenerActor_should_reject_connections_when_limit_reached() - { - var listenerOptions = new TcpListenerOptions { Host = "localhost", Port = 8080 }; - - var parentActor = Sys.ActorOf(Props.Create(() => new ParentForListener()), "parent-limited"); - - parentActor.Tell( - new ParentForListener.CreateListenerWithLimit( - listenerOptions, - MaxConcurrentConnections: 2), - TestActor); - - var listenerActor = ExpectMsg(cancellationToken: TestContext.Current.CancellationToken); - - // Send 2 connections - should be accepted - for (var i = 0; i < 2; i++) - { - var dummyFlow = CreateDummyConnectionFlow(); - listenerActor.Tell(new ListenerActor.IncomingConnection(dummyFlow), ActorRefs.NoSender); - } - - // Receive the 2 acceptance messages - for (var i = 0; i < 2; i++) - { - ExpectMsg( - cancellationToken: TestContext.Current.CancellationToken); - } - - // Send a 3rd connection - should be rejected - var rejectedFlow = CreateDummyConnectionFlow(); - listenerActor.Tell(new ListenerActor.IncomingConnection(rejectedFlow), ActorRefs.NoSender); - - // No ConnectionStarted message should arrive for the rejected connection - ExpectNoMsg(TimeSpan.FromMilliseconds(500), cancellationToken: TestContext.Current.CancellationToken); - } - - [Fact(Timeout = 5000)] - public void ListenerActor_should_not_accept_when_at_limit() - { - var listenerOptions = new TcpListenerOptions { Host = "localhost", Port = 8080 }; - - var parentActor = Sys.ActorOf(Props.Create(() => new ParentForListener()), "parent-at-limit"); - - parentActor.Tell( - new ParentForListener.CreateListenerWithLimit( - listenerOptions, - MaxConcurrentConnections: 1), - TestActor); - - var listenerActor = ExpectMsg(cancellationToken: TestContext.Current.CancellationToken); - - // Send first connection - should be accepted - var flow1 = CreateDummyConnectionFlow(); - listenerActor.Tell(new ListenerActor.IncomingConnection(flow1), ActorRefs.NoSender); - - var started1 = ExpectMsg( - cancellationToken: TestContext.Current.CancellationToken); - Assert.NotNull(started1); - - // Send second connection - should be rejected - var flow2 = CreateDummyConnectionFlow(); - listenerActor.Tell(new ListenerActor.IncomingConnection(flow2), ActorRefs.NoSender); - - // Verify no ConnectionStarted is sent for the rejected connection - ExpectNoMsg(TimeSpan.FromMilliseconds(500), cancellationToken: TestContext.Current.CancellationToken); - } -} diff --git a/src/TurboHTTP.Tests/Streams/Stages/Server/EntityDispatcherSpec.cs b/src/TurboHTTP.Tests/Streams/Stages/Server/EntityDispatcherSpec.cs deleted file mode 100644 index 536337bcb..000000000 --- a/src/TurboHTTP.Tests/Streams/Stages/Server/EntityDispatcherSpec.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Akka.Actor; -using Akka.Hosting; -using Akka.Streams.Dsl; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing; -using TurboHTTP.Streams.Stages.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Streams.Stages.Server; - -public sealed class EntityDispatcherSpec : StreamTestBase -{ - private static readonly TurboRequestDelegate NoOpPipeline = _ => Task.CompletedTask; - - private sealed class OrderActorKey; - - private sealed record GetOrder(string Id); - - private sealed record OrderResult(string Id, string Name); - - private sealed record DeleteOrder(string Id); - - private sealed record OrderDeleted; - - private sealed class OrderActor : ReceiveActor - { - public OrderActor() - { - Receive(msg => Sender.Tell(new OrderResult(msg.Id, "Widget"))); - Receive(_ => Sender.Tell(new OrderDeleted())); - } - } - - private TurboHttpContext CreateTestContext( - HttpMethod method, - string uri, - IServiceProvider services) - { - var path = new Uri(uri).PathAndQuery; - return ServerTestContext.Request() - .Method(method.Method) - .Path(path) - .Services(services) - .Materializer(Materializer) - .Build(); - } - - private (RouteTable Table, IServiceProvider Services) SetupAskRoute(IActorRef actorRef) - { - var registry = new ActorRegistry(); - registry.Register(actorRef); - - var services = new ServiceCollection() - .AddSingleton(registry) - .AddSingleton(registry) - .BuildServiceProvider(); - - var turboTable = new TurboRouteTable(); - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet((TurboHttpContext ctx) => new GetOrder(ctx.Request.RouteValues["id"]!.ToString()!)); - builder.UseActorRef(); - builder.Response((ctx, _) => - { - ctx.Response.StatusCode = 200; - return Task.CompletedTask; - }); - builder.Response((ctx, _) => - { - ctx.Response.StatusCode = 204; - return Task.CompletedTask; - }); - builder.AddToRouteTable(turboTable); - - return (turboTable.Freeze(), services); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_dispatch_ask_to_actor_and_return_mapped_response() - { - var actor = Sys.ActorOf(Props.Create(() => new OrderActor())); - var (table, services) = SetupAskRoute(actor); - - var stage = new RoutingStage(table, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext(HttpMethod.Get, "http://localhost/orders/42", services); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(200, result.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_return_202_for_tell_route() - { - var probe = CreateTestProbe(); - var registry = new ActorRegistry(); - registry.Register(probe.Ref); - - var services = new ServiceCollection() - .AddSingleton(registry) - .AddSingleton(registry) - .BuildServiceProvider(); - - var turboTable = new TurboRouteTable(); - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnPost((TurboHttpContext _) => new GetOrder("new")).Tell(); - builder.UseActorRef(); - builder.AddToRouteTable(turboTable); - - var stage = new RoutingStage(turboTable.Freeze(), NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext(HttpMethod.Post, "http://localhost/orders/1", services); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(202, result.Response.StatusCode); - probe.ExpectMsg(cancellationToken: TestContext.Current.CancellationToken); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_return_504_on_ask_timeout() - { - var probe = CreateTestProbe(); - var registry = new ActorRegistry(); - registry.Register(probe.Ref); - - var services = new ServiceCollection() - .AddSingleton(registry) - .AddSingleton(registry) - .BuildServiceProvider(); - - var turboTable = new TurboRouteTable(); - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet((TurboHttpContext ctx) => new GetOrder(ctx.Request.RouteValues["id"]!.ToString()!)); - builder.UseActorRef(); - builder.WithTimeout(TimeSpan.FromMilliseconds(100)); - builder.AddToRouteTable(turboTable); - - var stage = new RoutingStage(turboTable.Freeze(), NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext(HttpMethod.Get, "http://localhost/orders/42", services); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(504, result.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_return_500_when_no_response_mapper_found() - { - var actor = Sys.ActorOf(Props.Create(() => new OrderActor())); - var registry = new ActorRegistry(); - registry.Register(actor); - - var services = new ServiceCollection() - .AddSingleton(registry) - .AddSingleton(registry) - .BuildServiceProvider(); - - var turboTable = new TurboRouteTable(); - var builder = new TurboEntityBuilder("/orders/{id}"); - builder.OnGet((TurboHttpContext ctx) => new GetOrder(ctx.Request.RouteValues["id"]!.ToString()!)); - builder.UseActorRef(); - builder.AddToRouteTable(turboTable); - - var stage = new RoutingStage(turboTable.Freeze(), NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext(HttpMethod.Get, "http://localhost/orders/42", services); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(500, result.Response.StatusCode); - } -} diff --git a/src/TurboHTTP.Tests/Streams/Stages/Server/RoutingStageSpec.cs b/src/TurboHTTP.Tests/Streams/Stages/Server/RoutingStageSpec.cs deleted file mode 100644 index c1fadd2e9..000000000 --- a/src/TurboHTTP.Tests/Streams/Stages/Server/RoutingStageSpec.cs +++ /dev/null @@ -1,158 +0,0 @@ -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server; -using TurboHTTP.Routing; -using TurboHTTP.Streams.Stages.Server; -using TurboHTTP.Tests.Shared; - -namespace TurboHTTP.Tests.Streams.Stages.Server; - -public sealed class RoutingStageSpec : StreamTestBase -{ - private static readonly TurboRequestDelegate NoOpPipeline = _ => Task.CompletedTask; - - private TurboHttpContext CreateTestContext(string method, string uri) - { - var path = new Uri(uri).PathAndQuery; - return ServerTestContext.Request() - .Method(method) - .Path(path) - .Services(new ServiceCollection().BuildServiceProvider()) - .Materializer(Materializer) - .Build(); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_route_request_to_matching_handler() - { - var routeTable = new RouteTableBuilder() - .Add("GET", "/api/health", - new DelegateDispatcher(ctx => - { - ctx.Response.StatusCode = 200; - return Task.CompletedTask; - })) - .Build(); - - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/health"); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(200, result.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_return_404_for_unmatched_route() - { - var routeTable = new RouteTableBuilder().Build(); - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/unknown"); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(404, result.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_populate_route_values() - { - string? capturedId = null; - var routeTable = new RouteTableBuilder() - .Add("GET", "/api/orders/{id}", new DelegateDispatcher(ctx => - { - capturedId = ctx.Request.RouteValues["id"]?.ToString(); - ctx.Response.StatusCode = 200; - return Task.CompletedTask; - })) - .Build(); - - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/orders/42"); - - await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal("42", capturedId); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_return_500_on_dispatch_failure() - { - var routeTable = new RouteTableBuilder() - .Add("GET", "/api/fail", - new DelegateDispatcher(_ => throw new InvalidOperationException("boom"))) - .Build(); - - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/fail"); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(500, result.Response.StatusCode); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_push_context_when_StartAsync_called_before_handler_completes() - { - var handlerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var handlerRelease = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var routeTable = new RouteTableBuilder() - .Add("GET", "/api/stream", - new DelegateDispatcher(async ctx => - { - ctx.Response.StatusCode = 200; - ctx.Response.ContentType = "text/event-stream"; - await ctx.Features.Get()!.StartAsync(); - handlerStarted.SetResult(); - await handlerRelease.Task; - })) - .Build(); - - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/stream"); - - var resultTask = Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - await handlerStarted.Task; - - var result = await resultTask; - Assert.Equal(200, result.Response.StatusCode); - Assert.Equal("text/event-stream", result.Response.ContentType); - - handlerRelease.SetResult(); - } - - [Fact(Timeout = 5000)] - public async Task Stage_should_still_push_after_handler_completes_without_StartAsync() - { - var routeTable = new RouteTableBuilder() - .Add("GET", "/api/sync", - new DelegateDispatcher(async ctx => - { - await Task.Delay(50); - ctx.Response.StatusCode = 201; - })) - .Build(); - - var stage = new RoutingStage(routeTable, NoOpPipeline, 1, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)); - var ctx = CreateTestContext("GET", "http://localhost/api/sync"); - - var result = await Source.Single(ctx) - .Via(Flow.FromGraph(stage)) - .RunWith(Sink.First(), Materializer); - - Assert.Equal(201, result.Response.StatusCode); - } -} diff --git a/src/TurboHTTP.slnx b/src/TurboHTTP.slnx index 63f3c72bb..c517ce3fe 100644 --- a/src/TurboHTTP.slnx +++ b/src/TurboHTTP.slnx @@ -6,7 +6,7 @@ - + @@ -22,6 +22,8 @@ + + @@ -32,7 +34,7 @@ - + diff --git a/src/TurboHTTP/Client/Extensions.cs b/src/TurboHTTP/Client/Extensions.cs index 25d52a8ff..6bd62df9c 100644 --- a/src/TurboHTTP/Client/Extensions.cs +++ b/src/TurboHTTP/Client/Extensions.cs @@ -1,8 +1,7 @@ using Akka; -using Akka.Streams; using Akka.Streams.Dsl; using Servus.Akka.Streams.IO; -using TurboHTTP.Features.Sse; +using Servus.Akka.Sse; using TurboHTTP.Internal; namespace TurboHTTP.Client; diff --git a/src/TurboHTTP/Context/Adapters/TurboQueryCollection.cs b/src/TurboHTTP/Context/Adapters/TurboQueryCollection.cs deleted file mode 100644 index 944699f42..000000000 --- a/src/TurboHTTP/Context/Adapters/TurboQueryCollection.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; - -namespace TurboHTTP.Context.Adapters; - -internal sealed class TurboQueryCollection : IQueryCollection, ITurboQueryCollection -{ - private readonly Dictionary _store; - - public TurboQueryCollection(string? queryString) - { - if (string.IsNullOrEmpty(queryString)) - { - _store = new Dictionary(StringComparer.OrdinalIgnoreCase); - return; - } - - var parsed = QueryHelpers.ParseQuery(queryString); - _store = new Dictionary(parsed, StringComparer.OrdinalIgnoreCase); - } - - public StringValues this[string key] - => _store.TryGetValue(key, out var value) ? value : StringValues.Empty; - - public int Count => _store.Count; - public ICollection Keys => _store.Keys; - public bool ContainsKey(string key) => _store.ContainsKey(key); - public bool TryGetValue(string key, out StringValues value) => _store.TryGetValue(key, out value); - public IEnumerator> GetEnumerator() => _store.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/Adapters/TurboRequestCookieCollection.cs b/src/TurboHTTP/Context/Adapters/TurboRequestCookieCollection.cs deleted file mode 100644 index 914705a07..000000000 --- a/src/TurboHTTP/Context/Adapters/TurboRequestCookieCollection.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Http; - -namespace TurboHTTP.Context.Adapters; - -internal sealed class TurboRequestCookieCollection : IRequestCookieCollection, ITurboRequestCookieCollection -{ - private readonly Dictionary _cookies; - - public TurboRequestCookieCollection(string? cookieHeader) - { - _cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (string.IsNullOrEmpty(cookieHeader)) - { - return; - } - - foreach (var segment in cookieHeader.Split(';', - StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) - { - var eqIdx = segment.IndexOf('='); - if (eqIdx > 0) - { - var name = segment[..eqIdx].Trim(); - var value = segment[(eqIdx + 1)..].Trim(); - _cookies[name] = value; - } - } - } - - public string? this[string key] => _cookies.GetValueOrDefault(key); - - public int Count => _cookies.Count; - public ICollection Keys => _cookies.Keys; - public bool ContainsKey(string key) => _cookies.ContainsKey(key); - public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => _cookies.TryGetValue(key, out value); - public IEnumerator> GetEnumerator() => _cookies.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/Features/ITurboConnectionFeature.cs b/src/TurboHTTP/Context/Features/ITurboConnectionFeature.cs deleted file mode 100644 index a2f9477fa..000000000 --- a/src/TurboHTTP/Context/Features/ITurboConnectionFeature.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net; - -namespace TurboHTTP.Context.Features; - -public interface ITurboConnectionFeature -{ - string ConnectionId { get; set; } - IPAddress? LocalIpAddress { get; set; } - int LocalPort { get; set; } - IPAddress? RemoteIpAddress { get; set; } - int RemotePort { get; set; } -} diff --git a/src/TurboHTTP/Context/Features/ITurboFeatureCollection.cs b/src/TurboHTTP/Context/Features/ITurboFeatureCollection.cs deleted file mode 100644 index 9239777d7..000000000 --- a/src/TurboHTTP/Context/Features/ITurboFeatureCollection.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TurboHTTP.Context.Features; - -public interface ITurboFeatureCollection -{ - T? Get() where T : class; - void Set(T? feature) where T : class; -} diff --git a/src/TurboHTTP/Context/Features/ITurboRequestBodyDetectionFeature.cs b/src/TurboHTTP/Context/Features/ITurboRequestBodyDetectionFeature.cs deleted file mode 100644 index 6fe0dd515..000000000 --- a/src/TurboHTTP/Context/Features/ITurboRequestBodyDetectionFeature.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TurboHTTP.Context.Features; - -public interface ITurboRequestBodyDetectionFeature -{ - bool CanHaveBody { get; } -} diff --git a/src/TurboHTTP/Context/Features/ITurboRequestBodyFeature.cs b/src/TurboHTTP/Context/Features/ITurboRequestBodyFeature.cs deleted file mode 100644 index aefce84c8..000000000 --- a/src/TurboHTTP/Context/Features/ITurboRequestBodyFeature.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Akka; -using Akka.Streams.Dsl; - -namespace TurboHTTP.Context.Features; - -public interface ITurboRequestBodyFeature -{ - Stream Body { get; } - Source, NotUsed> BodySource { get; } -} diff --git a/src/TurboHTTP/Context/Features/ITurboRequestFeature.cs b/src/TurboHTTP/Context/Features/ITurboRequestFeature.cs deleted file mode 100644 index 33aca64f4..000000000 --- a/src/TurboHTTP/Context/Features/ITurboRequestFeature.cs +++ /dev/null @@ -1,16 +0,0 @@ -using TurboHTTP.Context; - -namespace TurboHTTP.Context.Features; - -public interface ITurboRequestFeature -{ - string Protocol { get; set; } - string Scheme { get; set; } - string Method { get; set; } - string PathBase { get; set; } - string Path { get; set; } - string QueryString { get; set; } - string RawTarget { get; set; } - ITurboHeaderDictionary Headers { get; } - Stream Body { get; set; } -} diff --git a/src/TurboHTTP/Context/Features/ITurboRequestIdentifierFeature.cs b/src/TurboHTTP/Context/Features/ITurboRequestIdentifierFeature.cs deleted file mode 100644 index 7eefa721c..000000000 --- a/src/TurboHTTP/Context/Features/ITurboRequestIdentifierFeature.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TurboHTTP.Context.Features; - -public interface ITurboRequestIdentifierFeature -{ - string TraceIdentifier { get; set; } -} diff --git a/src/TurboHTTP/Context/Features/ITurboRequestLifetimeFeature.cs b/src/TurboHTTP/Context/Features/ITurboRequestLifetimeFeature.cs deleted file mode 100644 index 08bf8b7ed..000000000 --- a/src/TurboHTTP/Context/Features/ITurboRequestLifetimeFeature.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TurboHTTP.Context.Features; - -public interface ITurboRequestLifetimeFeature -{ - CancellationToken RequestAborted { get; set; } - void Abort(); -} diff --git a/src/TurboHTTP/Context/Features/ITurboResetFeature.cs b/src/TurboHTTP/Context/Features/ITurboResetFeature.cs deleted file mode 100644 index 94fbfe252..000000000 --- a/src/TurboHTTP/Context/Features/ITurboResetFeature.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TurboHTTP.Context.Features; - -public interface ITurboResetFeature -{ - void Reset(int errorCode); -} diff --git a/src/TurboHTTP/Context/Features/ITurboResponseBodyFeature.cs b/src/TurboHTTP/Context/Features/ITurboResponseBodyFeature.cs deleted file mode 100644 index 9232465b8..000000000 --- a/src/TurboHTTP/Context/Features/ITurboResponseBodyFeature.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http.Features; - -namespace TurboHTTP.Context.Features; - -public interface ITurboResponseBodyFeature : IHttpResponseBodyFeature -{ - Sink, Task> BodySink { get; } - Task WhenSinkCompleted { get; } -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/Features/ITurboResponseFeature.cs b/src/TurboHTTP/Context/Features/ITurboResponseFeature.cs deleted file mode 100644 index 288ea737c..000000000 --- a/src/TurboHTTP/Context/Features/ITurboResponseFeature.cs +++ /dev/null @@ -1,14 +0,0 @@ -using TurboHTTP.Context; - -namespace TurboHTTP.Context.Features; - -public interface ITurboResponseFeature -{ - int StatusCode { get; set; } - string? ReasonPhrase { get; set; } - ITurboHeaderDictionary Headers { get; } - Stream Body { get; set; } - bool HasStarted { get; } - void OnStarting(Func callback, object? state); - void OnCompleted(Func callback, object? state); -} diff --git a/src/TurboHTTP/Context/Features/ITurboResponseTrailersFeature.cs b/src/TurboHTTP/Context/Features/ITurboResponseTrailersFeature.cs deleted file mode 100644 index 32a26019f..000000000 --- a/src/TurboHTTP/Context/Features/ITurboResponseTrailersFeature.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TurboHTTP.Context; - -namespace TurboHTTP.Context.Features; - -public interface ITurboResponseTrailersFeature -{ - ITurboHeaderDictionary Trailers { get; set; } -} diff --git a/src/TurboHTTP/Context/Features/TurboHttpConnectionFeature.cs b/src/TurboHTTP/Context/Features/TurboHttpConnectionFeature.cs deleted file mode 100644 index 53bfdeb0c..000000000 --- a/src/TurboHTTP/Context/Features/TurboHttpConnectionFeature.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; - -namespace TurboHTTP.Context.Features; - -internal sealed class TurboHttpConnectionFeature(TurboConnectionInfo info) : IHttpConnectionFeature, ITurboConnectionFeature -{ - private readonly TurboConnectionInfo _info = info ?? throw new ArgumentNullException(nameof(info)); - - public string ConnectionId - { - get => _info.Id; - set => _info.Id = value; - } - - public IPAddress? RemoteIpAddress - { - get => _info.RemoteIpAddress; - set => _info.RemoteIpAddress = value; - } - - public int RemotePort - { - get => _info.RemotePort; - set => _info.RemotePort = value; - } - - public IPAddress? LocalIpAddress - { - get => _info.LocalIpAddress; - set => _info.LocalIpAddress = value; - } - - public int LocalPort - { - get => _info.LocalPort; - set => _info.LocalPort = value; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs b/src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs deleted file mode 100644 index 049c979d8..000000000 --- a/src/TurboHTTP/Context/Features/TurboHttpRequestIdentifierFeature.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; - -namespace TurboHTTP.Context.Features; - -internal sealed class TurboHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature, ITurboRequestIdentifierFeature -{ - private readonly TurboHttpContext _context; - - public TurboHttpRequestIdentifierFeature(TurboHttpContext context) - { - _context = context; - } - - public string TraceIdentifier - { - get => _context.TraceIdentifier; - set => _context.TraceIdentifier = value; - } -} diff --git a/src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs b/src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs deleted file mode 100644 index adcddcba1..000000000 --- a/src/TurboHTTP/Context/Features/TurboHttpRequestLifetimeFeature.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; - -namespace TurboHTTP.Context.Features; - -internal sealed class TurboHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature, ITurboRequestLifetimeFeature -{ - private readonly TurboHttpContext _context; - - public TurboHttpRequestLifetimeFeature(TurboHttpContext context) - { - _context = context; - } - - public CancellationToken RequestAborted - { - get => _context.RequestAborted; - set => _context.RequestAborted = value; - } - - public void Abort() => _context.Abort(); -} diff --git a/src/TurboHTTP/Context/Features/TurboHttpResponseBodyFeature.cs b/src/TurboHTTP/Context/Features/TurboHttpResponseBodyFeature.cs deleted file mode 100644 index 7f8142fb8..000000000 --- a/src/TurboHTTP/Context/Features/TurboHttpResponseBodyFeature.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Buffers; -using System.IO.Pipelines; -using Akka; -using Akka.Streams.Dsl; -using Servus.Akka.Streams.IO; - -namespace TurboHTTP.Context.Features; - -internal sealed class TurboHttpResponseBodyFeature : ITurboResponseBodyFeature -{ - private readonly Pipe _pipe = new(); - private readonly TaskCompletionSource _headerCommit = new(TaskCreationOptions.RunContinuationsAsynchronously); - private Func? _onStarting; - private bool _completed; - - internal void SetOnStarting(Func onStarting) => _onStarting = onStarting; - - internal bool HasStarted { get; private set; } - - internal Task WhenHeadersReady => _headerCommit.Task; - - public Stream Stream => field ??= _pipe.Writer.AsStream(); - - public PipeWriter Writer => _pipe.Writer; - - public Task WhenSinkCompleted => Task.CompletedTask; - - public Sink, Task> BodySink - { - get - { - if (field == null) - { - var pipeSink = PipeSink.To(_pipe.Writer); - field = Flow.Create>() - .SelectAsync(1, async chunk => - { - await EnsureStartedAsync(); - return chunk; - }) - .ToMaterialized(pipeSink, Keep.Right); - } - - return field; - } - } - - public async Task StartAsync(CancellationToken cancellationToken = default) - { - await EnsureStartedAsync(); - } - - public async Task SendFileAsync(string path, long offset, long? count, - CancellationToken cancellationToken = default) - { - await EnsureStartedAsync(); - await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4 * 1024, - useAsync: true); - if (offset > 0) - { - fs.Seek(offset, SeekOrigin.Begin); - } - - var remaining = count ?? long.MaxValue; - var buffer = ArrayPool.Shared.Rent(4 * 1024); - try - { - while (remaining > 0) - { - var toRead = (int)Math.Min(buffer.Length, remaining); - var read = await fs.ReadAsync(buffer.AsMemory(0, toRead), cancellationToken); - if (read == 0) - { - break; - } - - var dest = _pipe.Writer.GetMemory(read); - buffer.AsSpan(0, read).CopyTo(dest.Span); - _pipe.Writer.Advance(read); - await _pipe.Writer.FlushAsync(cancellationToken); - remaining -= read; - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - internal void Complete() - { - if (!_completed) - { - _completed = true; - _pipe.Writer.Complete(); - } - } - - public async Task CompleteAsync() - { - if (!_completed) - { - _completed = true; - await _pipe.Writer.CompleteAsync(); - } - } - - public void DisableBuffering() - { - } - - internal Source, NotUsed> GetResponseSource() - { - return PipeSource.From(_pipe.Reader); - } - - internal Stream GetResponseStream() => _pipe.Reader.AsStream(); - - private async Task EnsureStartedAsync() - { - if (!HasStarted) - { - HasStarted = true; - if (_onStarting is not null) - { - await _onStarting(); - } - - _headerCommit.TrySetResult(); - } - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/Features/TurboRequestBodyFeature.cs b/src/TurboHTTP/Context/Features/TurboRequestBodyFeature.cs deleted file mode 100644 index ce1f9a3ff..000000000 --- a/src/TurboHTTP/Context/Features/TurboRequestBodyFeature.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Akka; -using Akka.Streams.Dsl; - -namespace TurboHTTP.Context.Features; - -internal sealed class TurboRequestBodyFeature : ITurboRequestBodyFeature -{ - public Stream Body { get; set; } = Stream.Null; - - public Source, NotUsed> BodySource { get; set; } - = Source.Empty>(); -} diff --git a/src/TurboHTTP/Context/TurboHttpRequest.cs b/src/TurboHTTP/Context/TurboHttpRequest.cs deleted file mode 100644 index 661cadece..000000000 --- a/src/TurboHTTP/Context/TurboHttpRequest.cs +++ /dev/null @@ -1,365 +0,0 @@ -using System.IO.Pipelines; -using Akka; -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using TurboHTTP.Context.Adapters; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Context; - -public sealed class TurboHttpRequest : HttpRequest -{ - private IFeatureCollection _features; - private HttpContext? _httpContext; - private IFormCollection? _parsedForm; - private Uri? _cachedRequestUri; - private IHttpRequestFeature? _requestFeature; - private IQueryCollection? _query; - private IRequestCookieCollection? _cookies; - private RouteValueDictionary? _routeValues; - private PipeReader? _bodyReader; - - public TurboHttpRequest(IFeatureCollection features) - { - _features = features ?? throw new ArgumentNullException(nameof(features)); - } - - private IHttpRequestFeature RequestFeature - => _requestFeature ??= _features.Get() ?? - throw new InvalidOperationException("IHttpRequestFeature not found in feature collection"); - - public override HttpContext HttpContext => _httpContext!; - - internal void SetHttpContext(HttpContext context) - { - _httpContext = context; - } - - public Uri? RequestUri - { - get - { - if (_cachedRequestUri is not null) - { - return _cachedRequestUri; - } - - var host = Host.Value; - if (string.IsNullOrEmpty(host)) - { - return null; - } - - var uriString = string.Concat(Scheme, "://", host, Path.Value, QueryString.Value); - _cachedRequestUri = new Uri(uriString); - return _cachedRequestUri; - } - } - - public HttpContent? Content - { - get - { - var feature = RequestFeature; - return feature.Body != Stream.Null ? new StreamContent(feature.Body) : null; - } - } - - public override string Method - { - get => RequestFeature.Method; - set => RequestFeature.Method = value; - } - - public override string Scheme - { - get => RequestFeature.Scheme; - set => RequestFeature.Scheme = value; - } - - public override bool IsHttps - { - get => Scheme == "https"; - set => Scheme = value ? "https" : "http"; - } - - public override HostString Host - { - get - { - var hostHeader = (string?)Headers["Host"] ?? string.Empty; - if (string.IsNullOrEmpty(hostHeader)) - { - // Fallback to extracted host from RequestUri if Host header is not set - var feature = RequestFeature; - if (feature is TurboHttpRequestFeature turboFeature && !string.IsNullOrEmpty(turboFeature.ExtractedHost)) - { - return new HostString(turboFeature.ExtractedHost); - } - } - return new HostString(hostHeader); - } - set => Headers["Host"] = value.Value ?? string.Empty; - } - - public override PathString PathBase - { - get => new(RequestFeature.PathBase); - set => RequestFeature.PathBase = value.Value ?? string.Empty; - } - - public override PathString Path - { - get => new(RequestFeature.Path); - set => RequestFeature.Path = value.Value ?? "/"; - } - - public override QueryString QueryString - { - get => new(RequestFeature.QueryString); - set => RequestFeature.QueryString = value.Value ?? string.Empty; - } - - public override IQueryCollection Query - { - get - { - _query ??= new TurboQueryCollection(RequestFeature.QueryString); - return _query; - } - set => _query = value; - } - - public override string Protocol - { - get => RequestFeature.Protocol; - set => RequestFeature.Protocol = value; - } - - public override IHeaderDictionary Headers => RequestFeature.Headers; - - public override IRequestCookieCollection Cookies - { - get - { - _cookies ??= new TurboRequestCookieCollection(Headers["Cookie"].ToString()); - return _cookies; - } - set => _cookies = value; - } - - public override long? ContentLength - { - get => Headers.ContentLength; - set => Headers.ContentLength = value; - } - - public override string? ContentType - { - get => (string?)Headers["Content-Type"] ?? string.Empty; - set => Headers["Content-Type"] = value ?? string.Empty; - } - - public override Stream Body - { - get => RequestFeature.Body; - set => RequestFeature.Body = value; - } - - public override PipeReader BodyReader - { - get - { - _bodyReader ??= PipeReader.Create(Body); - return _bodyReader; - } - } - - public Source, NotUsed> BodySource - => _features.Get()?.BodySource ?? Source.Empty>(); - - public override bool HasFormContentType - { - get - { - var contentType = ContentType; - if (string.IsNullOrEmpty(contentType)) - { - return false; - } - - return contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) - || contentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase); - } - } - - public override IFormCollection Form - { - get => _parsedForm ?? throw new InvalidOperationException("Form has not been read. Call ReadFormAsync first."); - set => _parsedForm = value; - } - - public override async Task ReadFormAsync(CancellationToken cancellationToken = default) - { - if (_parsedForm is not null) - { - return _parsedForm; - } - - var contentType = ContentType; - if (string.IsNullOrEmpty(contentType)) - { - _parsedForm = EmptyForm(); - return _parsedForm; - } - - if (contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) - { - _parsedForm = await ParseUrlEncodedFormAsync(cancellationToken); - return _parsedForm; - } - - if (contentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase)) - { - _parsedForm = await ParseMultipartFormAsync(contentType, cancellationToken); - return _parsedForm; - } - - _parsedForm = EmptyForm(); - return _parsedForm; - } - - public override RouteValueDictionary RouteValues - { - get => _routeValues ??= new RouteValueDictionary(); - set => _routeValues = value; - } - - private static IFormCollection EmptyForm() - { - return new TurboFormCollection( - new Dictionary(), - new TurboFormFileCollection([])); - } - - private async Task ParseUrlEncodedFormAsync(CancellationToken ct) - { - var feature = RequestFeature; - if (feature.Body == Stream.Null) - { - return EmptyForm(); - } - - using var reader = new StreamReader(feature.Body); - var body = await reader.ReadToEndAsync(ct); - var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries)) - { - var kv = pair.Split('=', 2); - if (kv.Length == 2) - { - var key = Uri.UnescapeDataString(kv[0]); - var value = Uri.UnescapeDataString(kv[1]); - if (fields.TryGetValue(key, out var existing)) - { - fields[key] = StringValues.Concat(existing, value); - } - else - { - fields[key] = value; - } - } - } - - return new TurboFormCollection(fields, new TurboFormFileCollection([])); - } - - private async Task ParseMultipartFormAsync(string contentType, CancellationToken ct) - { - var feature = RequestFeature; - if (feature.Body == Stream.Null) - { - return EmptyForm(); - } - - var boundary = ExtractBoundary(contentType); - if (boundary is null) - { - return EmptyForm(); - } - - var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); - var files = new List(); - - var reader = new MultipartReader(boundary, feature.Body); - - var section = await reader.ReadNextSectionAsync(ct); - while (section is not null) - { - if (ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var disposition)) - { - if (disposition.IsFileDisposition()) - { - var fileContent = new MemoryStream(); - await section.Body.CopyToAsync(fileContent, ct); - files.Add(new TurboFormFile( - disposition.Name.Value ?? string.Empty, - disposition.FileName.Value ?? string.Empty, - section.ContentType ?? "application/octet-stream", - fileContent.ToArray())); - } - else if (disposition.IsFormDisposition()) - { - using var sr = new StreamReader(section.Body); - var value = await sr.ReadToEndAsync(ct); - var name = disposition.Name.Value ?? string.Empty; - if (fields.TryGetValue(name, out var existing)) - { - fields[name] = StringValues.Concat(existing, value); - } - else - { - fields[name] = value; - } - } - } - - section = await reader.ReadNextSectionAsync(ct); - } - - return new TurboFormCollection(fields, new TurboFormFileCollection(files)); - } - - private static string? ExtractBoundary(string contentType) - { - var parts = contentType.Split(';'); - foreach (var part in parts) - { - var trimmed = part.Trim(); - if (trimmed.StartsWith("boundary=", StringComparison.OrdinalIgnoreCase)) - { - return trimmed["boundary=".Length..].Trim('"'); - } - } - - return null; - } - - internal void Reset(IFeatureCollection features) - { - _features = features; - _requestFeature = null; - _cachedRequestUri = null; - _parsedForm = null; - _query = null; - _cookies = null; - _routeValues = null; - _bodyReader = null; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Context/TurboHttpResponse.cs b/src/TurboHTTP/Context/TurboHttpResponse.cs deleted file mode 100644 index 4a5f3bb76..000000000 --- a/src/TurboHTTP/Context/TurboHttpResponse.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.IO.Pipelines; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Context; - -public sealed class TurboHttpResponse : HttpResponse -{ - private IFeatureCollection _features; - private HttpContext? _httpContext; - private IHttpResponseFeature? _responseFeature; - private IHttpResponseBodyFeature? _bodyFeature; - - public TurboHttpResponse(IFeatureCollection features) - { - _features = features ?? throw new ArgumentNullException(nameof(features)); - } - - private IHttpResponseFeature ResponseFeature - => _responseFeature ??= _features.Get() ?? throw new InvalidOperationException("IHttpResponseFeature not found in feature collection"); - - private IHttpResponseBodyFeature? BodyFeature - => _bodyFeature ??= _features.Get(); - - public override HttpContext HttpContext => _httpContext!; - - internal void SetHttpContext(HttpContext context) - { - _httpContext = context; - } - - public override int StatusCode - { - get => ResponseFeature.StatusCode; - set => ResponseFeature.StatusCode = value; - } - - public override IHeaderDictionary Headers => ResponseFeature.Headers; - - public override Stream Body - { - get => BodyFeature?.Stream ?? Stream.Null; - set { } - } - - public override PipeWriter BodyWriter => BodyFeature?.Writer ?? throw new InvalidOperationException("IHttpResponseBodyFeature not found in feature collection"); - - public override long? ContentLength - { - get => Headers.ContentLength; - set => Headers.ContentLength = value; - } - - public override string? ContentType - { - get => Headers["Content-Type"].ToString(); - set => Headers["Content-Type"] = value ?? string.Empty; - } - - public override IResponseCookies Cookies - => throw new NotSupportedException("Response cookies not yet supported."); - - public override bool HasStarted => ResponseFeature.HasStarted; - - public override void OnStarting(Func callback, object state) - { - ResponseFeature.OnStarting(callback, state); - } - - public override void OnCompleted(Func callback, object state) - { - ResponseFeature.OnCompleted(callback, state); - } - - public override void Redirect(string location, bool permanent = false) - { - ArgumentNullException.ThrowIfNull(location); - - if (location.AsSpan().ContainsAny('\r', '\n')) - { - throw new ArgumentException("Redirect location must not contain CR or LF characters.", nameof(location)); - } - - if (!location.StartsWith('/') && - Uri.TryCreate(location, UriKind.Absolute, out var uri) && - uri.Scheme is not ("http" or "https")) - { - throw new ArgumentException("Redirect location must be a relative path or an HTTP/HTTPS URL.", nameof(location)); - } - - StatusCode = permanent ? 301 : 302; - Headers["Location"] = location; - } - - public void DeclareTrailer(string name) - { - ArgumentNullException.ThrowIfNull(name); - - var existing = Headers["Trailer"].ToString(); - Headers["Trailer"] = string.IsNullOrEmpty(existing) - ? name - : string.Concat(existing, ", ", name); - } - - public void AppendTrailer(string name, string value) - { - ArgumentNullException.ThrowIfNull(name); - ArgumentNullException.ThrowIfNull(value); - - var feature = HttpContext.Features.Get(); - if (feature is null) - { - throw new InvalidOperationException( - "Response trailers are only supported on HTTP/2 and HTTP/3 connections."); - } - - feature.Trailers.Append(name, value); - } - - public IHeaderDictionary GetTrailers() - { - var feature = HttpContext.Features.Get(); - return feature?.Trailers ?? new HeaderDictionary(); - } - - internal void Reset(IFeatureCollection features) - { - _features = features; - _responseFeature = null; - _bodyFeature = null; - if (features.Get() is TurboHttpResponseFeature turboFeature) - { - turboFeature.Reset(); - } - if (features.Get() is TurboHttpResponseTrailersFeature trailersFeature) - { - trailersFeature.Reset(); - } - } -} diff --git a/src/TurboHTTP/Diagnostics/TurboServerInstrumentationExtensions.cs b/src/TurboHTTP/Diagnostics/TurboServerInstrumentationExtensions.cs new file mode 100644 index 000000000..47e957a32 --- /dev/null +++ b/src/TurboHTTP/Diagnostics/TurboServerInstrumentationExtensions.cs @@ -0,0 +1,123 @@ +using System.Diagnostics; +using Servus.Core.Diagnostics; + +namespace TurboHTTP.Diagnostics; + +internal static class TurboServerInstrumentationExtensions +{ + private static readonly HashSet StandardMethods = new(StringComparer.OrdinalIgnoreCase) + { + "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH" + }; + + public static bool IsServerTracingActive(this ServusTrace trace) + { + return trace.Source.HasListeners() + || Servus.Core.Servus.Metrics.ActiveConnections().Enabled + || Servus.Core.Servus.Metrics.ServerActiveRequests().Enabled + || Servus.Core.Servus.Metrics.ServerRequestDuration().Enabled; + } + + public static Activity? StartConnectionActivity(this ServusTrace trace, string serverAddress, int serverPort, string networkTransport) + { + if (!trace.Source.HasListeners()) + { + return null; + } + + var activity = trace.Source.StartActivity( + "TurboHTTP.Connection", + ActivityKind.Server); + + if (activity is null) + { + return null; + } + + activity.SetTag("server.address", serverAddress); + activity.SetTag("server.port", serverPort); + activity.SetTag("network.transport", networkTransport); + + return activity; + } + + public static void StopConnectionActivity(this ServusTrace _, Activity activity, Exception? error) + { + if (error is not null) + { + activity.SetStatus(ActivityStatusCode.Error, error.Message); + activity.SetTag("error.type", error.GetType().FullName); + } + + activity.Stop(); + } + + public static Activity? StartRequestActivity(this ServusTrace trace, string method, string path, string scheme) + { + if (!trace.Source.HasListeners()) + { + return null; + } + + var activity = trace.Source.StartActivity( + "TurboHTTP.ServerRequest", + ActivityKind.Server); + + if (activity is null) + { + return null; + } + + var normalizedMethod = NormalizeMethod(method); + activity.SetTag("http.request.method", normalizedMethod); + if (normalizedMethod == "_OTHER") + { + activity.SetTag("http.request.method_original", method); + } + + activity.SetTag("url.path", path); + activity.SetTag("url.scheme", scheme); + + return activity; + } + + public static void SetServerResponse(this ServusTrace _, Activity activity, int statusCode) + { + activity.SetTag("http.response.status_code", statusCode); + + if (statusCode >= 400) + { + activity.SetTag("error.type", statusCode.ToString()); + activity.SetStatus(ActivityStatusCode.Error); + } + } + + public static void SetServerError(this ServusTrace _, Activity activity, Exception exception) + { + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + activity.SetTag("error.type", exception.GetType().FullName); + activity.SetTag("exception.type", exception.GetType().FullName); + activity.SetTag("exception.message", exception.Message); + } + + public static void AddBackpressureEvent(this ServusTrace _, Activity activity, int inflight, int max) + { + activity.AddEvent(new ActivityEvent("turbo.backpressure", + tags: new ActivityTagsCollection + { + { "turbo.pipeline.inflight", inflight }, + { "turbo.pipeline.max", max } + })); + } + + public static void InjectConnectionTags(ref TagList tags, string serverAddress, int serverPort) + { + tags.Add("server.address", serverAddress); + tags.Add("server.port", serverPort); + } + + private static string NormalizeMethod(string method) + { + return StandardMethods.Contains(method) ? method.ToUpperInvariant() : "_OTHER"; + } +} diff --git a/src/TurboHTTP/Diagnostics/TurboServerMetricsExtensions.cs b/src/TurboHTTP/Diagnostics/TurboServerMetricsExtensions.cs new file mode 100644 index 000000000..2d50637b8 --- /dev/null +++ b/src/TurboHTTP/Diagnostics/TurboServerMetricsExtensions.cs @@ -0,0 +1,116 @@ +using System.Diagnostics.Metrics; +using Servus.Core.Diagnostics; + +namespace TurboHTTP.Diagnostics; + +internal static class TurboServerMetricsExtensions +{ + private static UpDownCounter? _activeConnections; + private static Histogram? _connectionDuration; + private static Counter? _rejectedConnections; + private static Histogram? _tlsHandshakeDuration; + private static UpDownCounter? _activeTlsHandshakes; + private static UpDownCounter? _serverActiveRequests; + private static Histogram? _serverRequestDuration; + private static UpDownCounter? _pipelineInFlight; + private static UpDownCounter? _pipelinePending; + private static Counter? _handlerTimeouts; + private static UpDownCounter? _drainActive; + private static Histogram? _protocolNegotiationDuration; + + public static UpDownCounter ActiveConnections(this ServusMetrics metrics) + { + return _activeConnections ??= metrics.Meter.CreateUpDownCounter( + "kestrel.active_connections", + unit: "{connection}", + description: "Number of connections that are currently active on the server."); + } + + public static Histogram ConnectionDuration(this ServusMetrics metrics) + { + return _connectionDuration ??= metrics.Meter.CreateHistogram( + "kestrel.connection.duration", + unit: "s", + description: "The duration of connections on the server."); + } + + public static Counter RejectedConnections(this ServusMetrics metrics) + { + return _rejectedConnections ??= metrics.Meter.CreateCounter( + "kestrel.rejected_connections", + unit: "{connection}", + description: "Number of connections rejected by the server."); + } + + public static Histogram TlsHandshakeDuration(this ServusMetrics metrics) + { + return _tlsHandshakeDuration ??= metrics.Meter.CreateHistogram( + "kestrel.tls_handshake.duration", + unit: "s", + description: "The duration of TLS handshakes on the server."); + } + + public static UpDownCounter ActiveTlsHandshakes(this ServusMetrics metrics) + { + return _activeTlsHandshakes ??= metrics.Meter.CreateUpDownCounter( + "kestrel.active_tls_handshakes", + unit: "{handshake}", + description: "Number of TLS handshakes that are currently in progress on the server."); + } + + public static UpDownCounter ServerActiveRequests(this ServusMetrics metrics) + { + return _serverActiveRequests ??= metrics.Meter.CreateUpDownCounter( + "http.server.active_requests", + unit: "{request}", + description: "Number of active HTTP server requests."); + } + + public static Histogram ServerRequestDuration(this ServusMetrics metrics) + { + return _serverRequestDuration ??= metrics.Meter.CreateHistogram( + "http.server.request.duration", + unit: "s", + description: "Duration of HTTP server requests."); + } + + public static UpDownCounter PipelineInFlight(this ServusMetrics metrics) + { + return _pipelineInFlight ??= metrics.Meter.CreateUpDownCounter( + "turbo.server.pipeline.inflight", + unit: "{request}", + description: "Number of requests currently being processed by the application handler."); + } + + public static UpDownCounter PipelinePending(this ServusMetrics metrics) + { + return _pipelinePending ??= metrics.Meter.CreateUpDownCounter( + "turbo.server.pipeline.pending", + unit: "{request}", + description: "Number of completed responses waiting in the reorder buffer."); + } + + public static Counter HandlerTimeouts(this ServusMetrics metrics) + { + return _handlerTimeouts ??= metrics.Meter.CreateCounter( + "turbo.server.handler.timeouts", + unit: "{timeout}", + description: "Number of application handler timeouts."); + } + + public static UpDownCounter DrainActive(this ServusMetrics metrics) + { + return _drainActive ??= metrics.Meter.CreateUpDownCounter( + "turbo.server.drain.active", + unit: "{connection}", + description: "Number of connections currently draining during graceful shutdown."); + } + + public static Histogram ProtocolNegotiationDuration(this ServusMetrics metrics) + { + return _protocolNegotiationDuration ??= metrics.Meter.CreateHistogram( + "turbo.server.protocol_negotiation.duration", + unit: "s", + description: "The duration of protocol negotiation."); + } +} diff --git a/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs b/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs index 2a60a9c1d..617f4ac04 100644 --- a/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs +++ b/src/TurboHTTP/Diagnostics/TurboTraceExtensions.cs @@ -49,4 +49,16 @@ public static MeterProviderBuilder AddTurboHttpInstrumentation(this MeterProvide return builder .AddMeter(Servus.Core.Servus.Metrics.Meter.Name); } + + public static TracerProviderBuilder AddTurboServerInstrumentation(this TracerProviderBuilder builder) + { + return builder + .AddSource(Servus.Core.Servus.Tracing.Source.Name); + } + + public static MeterProviderBuilder AddTurboServerInstrumentation(this MeterProviderBuilder builder) + { + return builder + .AddMeter(Servus.Core.Servus.Metrics.Meter.Name); + } } \ No newline at end of file diff --git a/src/TurboHTTP/Features/AltSvc/AltSvcParser.cs b/src/TurboHTTP/Features/AltSvc/AltSvcParser.cs index 1f26348a9..d1c226a7a 100644 --- a/src/TurboHTTP/Features/AltSvc/AltSvcParser.cs +++ b/src/TurboHTTP/Features/AltSvc/AltSvcParser.cs @@ -108,7 +108,7 @@ internal static List Parse(string headerValue, out bool isClear, Da } else if (param.StartsWith("persist=", StringComparison.OrdinalIgnoreCase)) { - persist = param.AsSpan(8).Trim().SequenceEqual("1"); + persist = param.AsSpan(8).Trim() is "1"; } } @@ -149,4 +149,4 @@ private static bool TryParseAuthority(string authority, out string host, out int return int.TryParse(portSpan, out port) && port is > 0 and <= 65535; } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Features/Caching/Cache.cs b/src/TurboHTTP/Features/Caching/Cache.cs index 2a0fba2ec..1f8856dfe 100644 --- a/src/TurboHTTP/Features/Caching/Cache.cs +++ b/src/TurboHTTP/Features/Caching/Cache.cs @@ -1,13 +1,13 @@ using System.Buffers; using System.Net; +using TurboHTTP.Protocol; using TurboHTTP.Protocol.Semantics; namespace TurboHTTP.Features.Caching; -internal sealed class Cache +internal sealed class Cache(ICacheStore store, CachePolicy? policy = null) { - private readonly ICacheStore _store; - private readonly CachePolicy _policy; + private readonly CachePolicy _policy = policy ?? CachePolicy.Default; private readonly LinkedList _lruOrder = []; private readonly Dictionary> _lruIndex = new(); @@ -16,17 +16,10 @@ internal sealed class Cache private readonly Dictionary varyValues)>> _variantIndex = new(); - public Cache(CachePolicy? policy = null) - : this(new MemoryCacheStore(), policy) + public Cache(CachePolicy? policy = null) : this(new MemoryCacheStore(), policy) { } - public Cache(ICacheStore store, CachePolicy? policy = null) - { - _store = store; - _policy = policy ?? CachePolicy.Default; - } - public int Count => _lruOrder.Count; public ICacheEntry? Get(HttpRequestMessage request) @@ -45,7 +38,7 @@ public Cache(ICacheStore store, CachePolicy? policy = null) continue; } - if (!_store.TryGet(compoundKey, out var storeEntry)) + if (!store.TryGet(compoundKey, out var storeEntry)) { continue; } @@ -115,12 +108,12 @@ public void Put( var lastKey = lastNode.Value; _lruOrder.RemoveLast(); _lruIndex.Remove(lastKey); - _store.Remove(lastKey); + store.Remove(lastKey); RemoveFromVariantIndex(lastKey); } - _store.Set(compoundKey, storeEntry); + store.Set(compoundKey, storeEntry); var lruNode = _lruOrder.AddFirst(compoundKey); _lruIndex[compoundKey] = lruNode; @@ -145,7 +138,7 @@ public void Invalidate(Uri uri) foreach (var (compoundKey, _) in variants.ToList()) { - _store.Remove(compoundKey); + store.Remove(compoundKey); if (_lruIndex.TryGetValue(compoundKey, out var node)) { @@ -182,18 +175,18 @@ public static bool ShouldStore(HttpRequestMessage request, HttpResponseMessage r return false; } - if (request.Headers.TryGetValues("Cache-Control", out var reqCcValues)) + if (request.Headers.TryGetValues(WellKnownHeaders.CacheControl, out var reqCcValues)) { - var reqCc = CacheControlParser.Parse(string.Join(", ", reqCcValues)); + var reqCc = CacheControlParser.Parse(string.Join(WellKnownHeaders.CommaSpace, reqCcValues)); if (reqCc?.NoStore == true) { return false; } } - if (response.Headers.TryGetValues("Cache-Control", out var resCcValues)) + if (response.Headers.TryGetValues(WellKnownHeaders.CacheControl, out var resCcValues)) { - var resCc = CacheControlParser.Parse(string.Join(", ", resCcValues)); + var resCc = CacheControlParser.Parse(string.Join(WellKnownHeaders.CommaSpace, resCcValues)); if (resCc?.NoStore == true) { return false; @@ -213,7 +206,7 @@ private static bool IsUnderstoodStatusCode(HttpResponseMessage response) public void Clear() { - _store.Clear(); + store.Clear(); _lruOrder.Clear(); _lruIndex.Clear(); _variantIndex.Clear(); @@ -271,9 +264,9 @@ private static CacheStoreEntry BuildStoreEntry( HttpRequestMessage request) { CacheControl? cc = null; - if (response.Headers.TryGetValues("Cache-Control", out var ccValues)) + if (response.Headers.TryGetValues(WellKnownHeaders.CacheControl, out var ccValues)) { - cc = CacheControlParser.Parse(string.Join(", ", ccValues)); + cc = CacheControlParser.Parse(string.Join(WellKnownHeaders.CommaSpace, ccValues)); } string? etag = null; @@ -321,7 +314,7 @@ private static CacheStoreEntry BuildStoreEntry( string? reqValue = null; if (request.Headers.TryGetValues(name, out var reqHeaderValues)) { - reqValue = string.Join(", ", reqHeaderValues); + reqValue = string.Join(WellKnownHeaders.CommaSpace, reqHeaderValues); } varyRequestValues[name] = reqValue; @@ -358,7 +351,7 @@ private static bool VaryMatchesRequest( string? currentValue = null; if (request.Headers.TryGetValues(name, out var vals)) { - currentValue = string.Join(", ", vals); + currentValue = string.Join(WellKnownHeaders.CommaSpace, vals); } if (!string.Equals(cachedValue, currentValue, StringComparison.Ordinal)) @@ -385,29 +378,32 @@ private void RemoveMatching(string primaryKey, IReadOnlyDictionary= 0; i--) { - if (variants[i].compoundKey == compoundKey) + if (variants[i].compoundKey != compoundKey) { - variants.RemoveAt(i); - break; + continue; } + + variants.RemoveAt(i); + break; } - if (variants.Count == 0) + if (variants.Count != 0) { - _variantIndex.Remove(primaryKey); - break; + continue; } + _variantIndex.Remove(primaryKey); + break; } } @@ -445,12 +444,12 @@ private static string NormalizeUri(Uri uri) private static void StripPrivateFields(HttpResponseMessage response) { - if (!response.Headers.TryGetValues("Cache-Control", out var ccValues)) + if (!response.Headers.TryGetValues(WellKnownHeaders.CacheControl, out var ccValues)) { return; } - var cc = CacheControlParser.Parse(string.Join(", ", ccValues)); + var cc = CacheControlParser.Parse(string.Join(WellKnownHeaders.CommaSpace, ccValues)); if (cc?.PrivateFields is not { Count: > 0 } fields) { return; @@ -465,30 +464,32 @@ private static void StripPrivateFields(HttpResponseMessage response) private static void StripConnectionHeaders(HttpResponseMessage response) { - if (response.Headers.TryGetValues("Connection", out var connectionValues)) + if (response.Headers.TryGetValues(WellKnownHeaders.Connection, out var connectionValues)) { foreach (var value in connectionValues) { foreach (var field in value.Split(',')) { var trimmed = field.Trim(); - if (trimmed.Length > 0) + if (trimmed.Length <= 0) { - response.Headers.Remove(trimmed); - response.Content?.Headers.Remove(trimmed); + continue; } + + response.Headers.Remove(trimmed); + response.Content?.Headers.Remove(trimmed); } } } - response.Headers.Remove("Connection"); - response.Headers.Remove("Keep-Alive"); - response.Headers.Remove("Proxy-Authenticate"); - response.Headers.Remove("Proxy-Authorization"); - response.Headers.Remove("TE"); - response.Headers.Remove("Trailer"); - response.Headers.Remove("Transfer-Encoding"); - response.Headers.Remove("Upgrade"); + response.Headers.Remove(WellKnownHeaders.Connection); + response.Headers.Remove(WellKnownHeaders.KeepAliveHeader); + response.Headers.Remove(WellKnownHeaders.ProxyAuthenticate); + response.Headers.Remove(WellKnownHeaders.ProxyAuthorization); + response.Headers.Remove(WellKnownHeaders.Te); + response.Headers.Remove(WellKnownHeaders.Trailer); + response.Headers.Remove(WellKnownHeaders.TransferEncoding); + response.Headers.Remove(WellKnownHeaders.Upgrade); } private static string GetVaryKey(IReadOnlyDictionary varyRequestValues) @@ -505,27 +506,20 @@ private static string GetVaryKey(IReadOnlyDictionary varyReques return string.Join("&", parts); } - private sealed class CacheStoreEntryAdapter : ICacheEntry + private sealed class CacheStoreEntryAdapter(CacheStoreEntry entry) : ICacheEntry { - private readonly CacheStoreEntry _entry; - - public CacheStoreEntryAdapter(CacheStoreEntry entry) - { - _entry = entry; - } - - public HttpResponseMessage Response => _entry.Response; - public ReadOnlyMemory Body => _entry.Body.Memory; - public DateTimeOffset RequestTime => _entry.RequestTime; - public DateTimeOffset ResponseTime => _entry.ResponseTime; - public string? ETag => _entry.ETag; - public DateTimeOffset? LastModified => _entry.LastModified; - public DateTimeOffset? Expires => _entry.Expires; - public DateTimeOffset? Date => _entry.Date; - public int? AgeSeconds => _entry.AgeSeconds; - public CacheControl? CacheControl => _entry.CacheControl?.ToCacheControl(); - public IReadOnlyList VaryHeaderNames => _entry.VaryHeaderNames; - public IReadOnlyDictionary VaryRequestValues => _entry.VaryRequestValues; + public HttpResponseMessage Response => entry.Response; + public ReadOnlyMemory Body => entry.Body.Memory; + public DateTimeOffset RequestTime => entry.RequestTime; + public DateTimeOffset ResponseTime => entry.ResponseTime; + public string? ETag => entry.ETag; + public DateTimeOffset? LastModified => entry.LastModified; + public DateTimeOffset? Expires => entry.Expires; + public DateTimeOffset? Date => entry.Date; + public int? AgeSeconds => entry.AgeSeconds; + public CacheControl? CacheControl => entry.CacheControl?.ToCacheControl(); + public IReadOnlyList VaryHeaderNames => entry.VaryHeaderNames; + public IReadOnlyDictionary VaryRequestValues => entry.VaryRequestValues; public void Dispose() { diff --git a/src/TurboHTTP/Features/Caching/CacheFreshnessEvaluator.cs b/src/TurboHTTP/Features/Caching/CacheFreshnessEvaluator.cs index a1f23a41d..67f79d340 100644 --- a/src/TurboHTTP/Features/Caching/CacheFreshnessEvaluator.cs +++ b/src/TurboHTTP/Features/Caching/CacheFreshnessEvaluator.cs @@ -1,3 +1,5 @@ +using TurboHTTP.Protocol; + namespace TurboHTTP.Features.Caching; /// @@ -131,8 +133,8 @@ public static void InjectAgeHeader(HttpResponseMessage response, ICacheEntry ent } // Remove existing Age header and set the correct value - response.Headers.Remove("Age"); - response.Headers.TryAddWithoutValidation("Age", ageSeconds.ToString()); + response.Headers.Remove(WellKnownHeaders.Age); + response.Headers.TryAddWithoutValidation(WellKnownHeaders.Age, ageSeconds.ToString()); } /// @@ -149,8 +151,8 @@ public static CacheLookupResult Evaluate(ICacheEntry? entry, HttpRequestMessage } // Parse request Cache-Control - var reqCc = request.Headers.TryGetValues("Cache-Control", out var ccValues) - ? CacheControlParser.Parse(string.Join(", ", ccValues)) + var reqCc = request.Headers.TryGetValues(WellKnownHeaders.CacheControl, out var ccValues) + ? CacheControlParser.Parse(string.Join(WellKnownHeaders.CommaSpace, ccValues)) : null; // RFC 9111 §5.2.1.4 — no-cache forces revalidation @@ -211,4 +213,4 @@ public static CacheLookupResult Evaluate(ICacheEntry? entry, HttpRequestMessage return CacheLookupResult.MustRevalidate(entry, $"RFC 9111 §4.2: Entry is stale (lifetime={freshnessLifetime.TotalSeconds:F0}s, age={currentAge.TotalSeconds:F0}s)."); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Features/Caching/CacheLookupResult.cs b/src/TurboHTTP/Features/Caching/CacheLookupResult.cs index 03491f449..8044190de 100644 --- a/src/TurboHTTP/Features/Caching/CacheLookupResult.cs +++ b/src/TurboHTTP/Features/Caching/CacheLookupResult.cs @@ -1,6 +1,12 @@ namespace TurboHTTP.Features.Caching; -internal enum CacheLookupStatus { Miss, Fresh, Stale, MustRevalidate } +internal enum CacheLookupStatus +{ + Miss, + Fresh, + Stale, + MustRevalidate +} internal sealed record CacheLookupResult { @@ -19,4 +25,4 @@ public static CacheLookupResult Stale(ICacheEntry entry, string reason) public static CacheLookupResult MustRevalidate(ICacheEntry entry, string reason) => new() { Status = CacheLookupStatus.MustRevalidate, Entry = entry, Reason = reason }; -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Features/Caching/CacheValidationRequestBuilder.cs b/src/TurboHTTP/Features/Caching/CacheValidationRequestBuilder.cs index 5132cdcaf..38fd0b57e 100644 --- a/src/TurboHTTP/Features/Caching/CacheValidationRequestBuilder.cs +++ b/src/TurboHTTP/Features/Caching/CacheValidationRequestBuilder.cs @@ -1,4 +1,5 @@ using System.Net; +using TurboHTTP.Protocol; using TurboHTTP.Protocol.Semantics; namespace TurboHTTP.Features.Caching; @@ -148,7 +149,7 @@ public static bool TryFreshenFromHead(HttpResponseMessage headResponse, CacheEnt foreach (var header in headResponse.Headers) { // Skip ETag — already validated - if (string.Equals(header.Key, "ETag", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(header.Key, WellKnownHeaders.ETag, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/src/TurboHTTP/Features/Cookies/CookieJar.cs b/src/TurboHTTP/Features/Cookies/CookieJar.cs index a8aafd7b9..6dd7fe9d4 100644 --- a/src/TurboHTTP/Features/Cookies/CookieJar.cs +++ b/src/TurboHTTP/Features/Cookies/CookieJar.cs @@ -1,3 +1,6 @@ +using System.Net; +using TurboHTTP.Protocol; + namespace TurboHTTP.Features.Cookies; internal sealed class CookieJar @@ -23,7 +26,7 @@ public void ProcessResponse(Uri requestUri, HttpResponseMessage response) var now = DateTimeOffset.UtcNow; - if (!response.Headers.TryGetValues("Set-Cookie", out var setCookieValues)) + if (!response.Headers.TryGetValues(WellKnownHeaders.SetCookie, out var setCookieValues)) { return; } @@ -104,7 +107,8 @@ public void AddCookiesToRequest(Uri requestUri, ref HttpRequestMessage request) parts[i] = $"{_applicable[i].Name}={_applicable[i].Value}"; } - request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", parts)); + request.Headers.TryAddWithoutValidation(WellKnownHeaders.Cookie, + string.Join(WellKnownHeaders.SemiColonSpace, parts)); } public int Count => _store.Count; @@ -168,7 +172,7 @@ private static bool IsExpired(CookieStoreEntry cookie, DateTimeOffset now) private static bool IsIpAddress(string host) { - return System.Net.IPAddress.TryParse(host, out _); + return IPAddress.TryParse(host, out _); } private static CookieStoreEntry ToStoreEntry(CookieEntry entry) => new( diff --git a/src/TurboHTTP/Internal/ClientCorrelationKeys.cs b/src/TurboHTTP/Internal/ClientCorrelationKeys.cs index b0e256cc0..ab86abbae 100644 --- a/src/TurboHTTP/Internal/ClientCorrelationKeys.cs +++ b/src/TurboHTTP/Internal/ClientCorrelationKeys.cs @@ -5,6 +5,4 @@ internal static class OptionsKey internal static readonly HttpRequestOptionsKey ConsumerIdKey = new("TurboHTTP.ConsumerId"); internal static readonly HttpRequestOptionsKey Key = new("TurboHTTP.PendingRequest"); internal static readonly HttpRequestOptionsKey VersionKey = new("TurboHTTP.Version"); - internal static readonly HttpRequestOptionsKey Http2 = new("TurboHTTP.StreamId.H2"); - internal static readonly HttpRequestOptionsKey Http3 = new("TurboHTTP.StreamId.H3"); } \ No newline at end of file diff --git a/src/TurboHTTP/Internal/CompressingContent.cs b/src/TurboHTTP/Internal/CompressingContent.cs index a8eb0ff79..b29bfd87c 100644 --- a/src/TurboHTTP/Internal/CompressingContent.cs +++ b/src/TurboHTTP/Internal/CompressingContent.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Net; +using TurboHTTP.Protocol; using TurboHTTP.Protocol.Semantics; namespace TurboHTTP.Internal; @@ -13,8 +14,8 @@ public CompressingContent(HttpContent inner, string encoding) { foreach (var header in inner.Headers) { - if (header.Key.Equals("Content-Encoding", StringComparison.OrdinalIgnoreCase) || - header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) + if (header.Key.Equals(WellKnownHeaders.ContentEncoding, StringComparison.OrdinalIgnoreCase) || + header.Key.Equals(WellKnownHeaders.ContentLength, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -22,7 +23,7 @@ public CompressingContent(HttpContent inner, string encoding) Headers.TryAddWithoutValidation(header.Key, header.Value); } - Headers.TryAddWithoutValidation("Content-Encoding", encoding); + Headers.TryAddWithoutValidation(WellKnownHeaders.ContentEncoding, encoding); using var output = RecyclableStreams.Manager.GetStream(); using (var compressor = ContentEncoding.CreateCompressor(output, encoding)) diff --git a/src/TurboHTTP/Internal/OptionsFactory.cs b/src/TurboHTTP/Internal/OptionsFactory.cs index 7b497f8e8..188f87dca 100644 --- a/src/TurboHTTP/Internal/OptionsFactory.cs +++ b/src/TurboHTTP/Internal/OptionsFactory.cs @@ -11,7 +11,6 @@ internal static class PoolKeys internal const string Http2 = "http2"; } - internal static class OptionsFactory { internal static TransportOptions Build(RequestEndpoint endpoint, TurboClientOptions clientOptions) @@ -90,5 +89,4 @@ internal static TransportOptions Build(RequestEndpoint endpoint, TurboClientOpti DefaultProxyCredentials = clientOptions.DefaultProxyCredentials, }; } -} - +} \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/IServerStateMachine.cs b/src/TurboHTTP/Protocol/IServerStateMachine.cs index 99c5fc4a4..9274a9c24 100644 --- a/src/TurboHTTP/Protocol/IServerStateMachine.cs +++ b/src/TurboHTTP/Protocol/IServerStateMachine.cs @@ -1,5 +1,5 @@ +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Server; namespace TurboHTTP.Protocol; @@ -10,7 +10,7 @@ internal interface IServerStateMachine int MaxQueuedRequests { get; } void PreStart(); - void OnResponse(TurboHttpContext context); + void OnResponse(IFeatureCollection features); void DecodeClientData(ITransportInbound data); void OnDownstreamFinished(); void OnTimerFired(string name); diff --git a/src/TurboHTTP/Protocol/LineBased/Body/BodyEncoderFactory.cs b/src/TurboHTTP/Protocol/LineBased/Body/BodyEncoderFactory.cs index d2cffa5c4..46fc1fa5c 100644 --- a/src/TurboHTTP/Protocol/LineBased/Body/BodyEncoderFactory.cs +++ b/src/TurboHTTP/Protocol/LineBased/Body/BodyEncoderFactory.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net.Http.Headers; namespace TurboHTTP.Protocol.LineBased.Body; diff --git a/src/TurboHTTP/Protocol/Multiplexed/Body/StreamBodyMessages.cs b/src/TurboHTTP/Protocol/Multiplexed/Body/StreamBodyMessages.cs index 7364ddd7d..60d47d87d 100644 --- a/src/TurboHTTP/Protocol/Multiplexed/Body/StreamBodyMessages.cs +++ b/src/TurboHTTP/Protocol/Multiplexed/Body/StreamBodyMessages.cs @@ -2,7 +2,10 @@ namespace TurboHTTP.Protocol.Multiplexed.Body; -internal sealed record StreamBodyChunk(T StreamId, IMemoryOwner Owner, int Length); +internal sealed record StreamBodyChunk(T StreamId, IMemoryOwner Owner, int Length, int Offset = 0) +{ + public ReadOnlyMemory Data => Owner.Memory.Slice(Offset, Length); +} internal sealed record StreamBodyComplete(T StreamId); diff --git a/src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs b/src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs index bcbcd8e43..51cdb6502 100644 --- a/src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs +++ b/src/TurboHTTP/Protocol/ProtocolNegotiatingStateMachine.cs @@ -1,11 +1,13 @@ using System.Net.Security; using Akka.Actor; using Akka.Event; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; +using TurboHTTP.Protocol.Syntax.Http10.Server; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Streams.Stages.Server; namespace TurboHTTP.Protocol; @@ -55,7 +57,7 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) => _inner!.OnResponse(context); + public void OnResponse(IFeatureCollection features) => _inner!.OnResponse(features); public void OnDownstreamFinished() => _inner?.OnDownstreamFinished(); public void OnTimerFired(string name) => _inner?.OnTimerFired(name); public void OnBodyMessage(object msg) => _inner?.OnBodyMessage(msg); @@ -91,6 +93,9 @@ private void OnWaitingForConnect(ITransportInbound data) _phase = Phase.Sniffing; } + private static ReadOnlySpan Http2PrefixMagic => "PRI "u8; + private static ReadOnlySpan Http10VersionTag => "HTTP/1.0\r\n"u8; + private void OnSniffing(ITransportInbound data) { _buffered.Add(data); @@ -106,18 +111,55 @@ private void OnSniffing(ITransportInbound data) return; } - if (span[0] == 'P' && span[1] == 'R' && span[2] == 'I' && span[3] == ' ') + if (span.StartsWith(Http2PrefixMagic)) { Activate(ops => new Http2ServerStateMachine(_options, ops)); + ReplayBuffered(); + return; } - else + + if (DetectHttp10()) + { + Activate(ops => new Http10ServerStateMachine(_options, ops)); + } + else if (ContainsRequestLineCrlf()) { Activate(ops => new Http11ServerStateMachine(_options, ops)); } + else + { + return; + } ReplayBuffered(); } + private bool DetectHttp10() + { + foreach (var item in _buffered) + { + if (item is TransportData { Buffer: var buf } && buf.Memory.Span.IndexOf(Http10VersionTag) >= 0) + { + return true; + } + } + + return false; + } + + private bool ContainsRequestLineCrlf() + { + foreach (var item in _buffered) + { + if (item is TransportData { Buffer: var buf } && buf.Memory.Span.IndexOf((byte)'\n') >= 0) + { + return true; + } + } + + return false; + } + private void Activate(Func factory) { _inner = factory(_wrappedOps); @@ -167,7 +209,7 @@ public UpgradeAwareOps(IServerStageOperations real, ProtocolNegotiatingStateMach _parent = parent; } - public void OnRequest(TurboHttpContext context) => _real.OnRequest(context); + public void OnRequest(IFeatureCollection features) => _real.OnRequest(features); public void OnOutbound(ITransportOutbound item) => _real.OnOutbound(item); public void OnScheduleTimer(string name, TimeSpan delay) => _real.OnScheduleTimer(name, delay); public void OnCancelTimer(string name) => _real.OnCancelTimer(name); @@ -175,7 +217,7 @@ public UpgradeAwareOps(IServerStageOperations real, ProtocolNegotiatingStateMach public IActorRef StageActor => _real.StageActor; public Akka.Streams.IMaterializer Materializer => _real.Materializer; public IServiceProvider? Services => _real.Services; - public TurboConnectionInfo? ConnectionInfo => _real.ConnectionInfo; + public TurboHttpConnectionFeature? ConnectionFeature => _real.ConnectionFeature; public TlsHandshakeFeature? TlsHandshakeFeature => _real.TlsHandshakeFeature; public void RequestProtocolSwitch(Func newSmFactory) diff --git a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerDecoder.cs b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerDecoder.cs index 80e8250fd..3b022f725 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerDecoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerDecoder.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.LineBased; using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http10.Options; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http10.Server; diff --git a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerEncoder.cs index f96604ef8..7a3eebc8b 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerEncoder.cs @@ -1,12 +1,9 @@ using System.Net; using Akka.Actor; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.LineBased; -using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http10.Options; -using TurboHTTP.Server; namespace TurboHTTP.Protocol.Syntax.Http10.Server; @@ -21,31 +18,37 @@ public Http10ServerEncoder(Http10ServerEncoderOptions options) _options = options; } - public int Encode(Span _, TurboHttpContext context, IActorRef stageActor) + public int Encode(Span _, IFeatureCollection features, IActorRef stageActor) { // HTTP/1.0 always defers — body sink will be handled by caller return 0; } - public int EncodeDeferred(Span destination, TurboHttpContext context, ReadOnlySpan body) + public int EncodeDeferred(Span destination, IFeatureCollection features, ReadOnlySpan body) { var writer = SpanWriter.Create(destination); - StatusLineWriter.Write(ref writer, HttpVersion.Version10, context.Response.StatusCode); + var responseFeature = features.Get(); + var statusCode = responseFeature?.StatusCode ?? 500; + StatusLineWriter.Write(ref writer, HttpVersion.Version10, statusCode); _reusableHeaders.Clear(); var headers = _reusableHeaders; - foreach (var h in context.Response.Headers) + var responseHeaders = responseFeature?.Headers; + if (responseHeaders is not null) { - if (ConnectionSemantics.IsHopByHop(h.Key)) + foreach (var h in responseHeaders) { - continue; - } + if (ConnectionSemantics.IsHopByHop(h.Key)) + { + continue; + } - foreach (var v in h.Value) - { - if (v is not null) + foreach (var v in h.Value) { - headers.Add(h.Key, v); + if (v is not null) + { + headers.Add(h.Key, v); + } } } } diff --git a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs index f3bcfde49..d9a617e5d 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http10/Server/Http10ServerStateMachine.cs @@ -1,15 +1,15 @@ using System.Buffers; using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Syntax.Http10.Options; using TurboHTTP.Server; -using TurboHTTP.Streams; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Streams.Stages.Server; using static Servus.Core.Servus; using HttpVersion = System.Net.HttpVersion; + namespace TurboHTTP.Protocol.Syntax.Http10.Server; internal sealed class Http10ServerStateMachine : IServerStateMachine @@ -18,20 +18,23 @@ internal sealed class Http10ServerStateMachine : IServerStateMachine private readonly Http10ServerDecoder _decoder; private readonly Http10ServerEncoder _encoder; private readonly long _maxRequestBodySize; + private readonly TurboServerOptions _serverOptions; - private TurboHttpContext? _deferredContext; + private IFeatureCollection? _deferredFeatures; private IMemoryOwner? _deferredBodyOwner; private int _deferredBodyLength; private IBodyEncoder? _activeBodyEncoder; + private bool _errorOccurred; public bool CanAcceptResponse => true; - public bool ShouldComplete { get; private set; } + public bool ShouldComplete => _errorOccurred; public int MaxQueuedRequests => 1; public Http10ServerStateMachine(TurboServerOptions options, IServerStageOperations ops) { _ops = ops ?? throw new ArgumentNullException(nameof(ops)); ArgumentNullException.ThrowIfNull(options); + _serverOptions = options; _maxRequestBodySize = options.Http1.MaxRequestBodySize; var shared = SharedHttpOptions.Default with @@ -56,6 +59,7 @@ public void PreStart() public void DecodeClientData(ITransportInbound data) { + if (data is not TransportData { Buffer: var buffer }) { return; @@ -70,18 +74,18 @@ public void DecodeClientData(ITransportInbound data) var outcome = _decoder.Feed(buffer.Memory.Span, out _); + if (outcome == DecodeOutcome.Complete) { - ShouldComplete = true; var feature = _decoder.GetRequestFeature(); var hasBody = feature.Body != Stream.Null; - var context = ServerContextFactory.Create(feature, hasBody, _ops.Services, _ops.ConnectionInfo, _ops.TlsHandshakeFeature); - _ops.OnRequest(context); + var features = FeatureCollectionFactory.Create(feature, hasBody, _ops.Services, _ops.ConnectionFeature, _ops.TlsHandshakeFeature, _serverOptions.Limits.MaxRequestBodySize); + _ops.OnRequest(features); } } catch (Exception) { - ShouldComplete = true; + _errorOccurred = true; } finally { @@ -89,11 +93,12 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) + public void OnResponse(IFeatureCollection features) { - _deferredContext = context; - var responseBody = context.Features.Get(); + _deferredFeatures = features; + + var responseBody = features.Get(); if (responseBody is TurboHttpResponseBodyFeature turboBody) { var bodyStream = turboBody.GetResponseStream(); @@ -101,9 +106,12 @@ public void OnResponse(TurboHttpContext context) if (encoder is not null) { _activeBodyEncoder = encoder; - encoder.Start(bodyStream, _ops.StageActor); + encoder.Start(bodyStream!, _ops.StageActor); + return; } } + + EncodeDeferredResponse(ReadOnlySpan.Empty); } public void OnDownstreamFinished() @@ -116,56 +124,72 @@ public void OnTimerFired(string name) public void OnBodyMessage(object msg) { + switch (msg) { - case OutboundBodyChunk chunk when _deferredContext is not null: + case OutboundBodyChunk chunk when _deferredFeatures is not null: _deferredBodyOwner?.Dispose(); _deferredBodyOwner = chunk.Owner; _deferredBodyLength = chunk.Length; break; - case OutboundBodyComplete when _deferredContext is not null && _deferredBodyOwner is not null: - TransportBuffer? item = null; - try - { - var body = _deferredBodyOwner.Memory.Span[.._deferredBodyLength]; - var bufferSize = 8192 + _deferredBodyLength; - item = TransportBuffer.Rent(bufferSize); - var written = _encoder.EncodeDeferred(item.FullMemory.Span, _deferredContext, body); - item.Length = written; - _ops.OnOutbound(new TransportData(item)); - } - catch (Exception ex) - { - item?.Dispose(); - Tracing.For("Protocol").Error(this, "Failed to encode HTTP/1.0 response body: {0}", ex.Message); - } - finally - { - _deferredBodyOwner.Dispose(); - _deferredBodyOwner = null; - _deferredContext = null; - } + case OutboundBodyComplete when _deferredFeatures is not null: + var body = _deferredBodyOwner is not null + ? _deferredBodyOwner.Memory.Span[.._deferredBodyLength] + : ReadOnlySpan.Empty; + EncodeDeferredResponse(body); + _deferredBodyOwner?.Dispose(); + _deferredBodyOwner = null; break; case OutboundBodyFailed failed: _deferredBodyOwner?.Dispose(); _deferredBodyOwner = null; - if (_deferredContext is not null) + if (_deferredFeatures is not null) { Tracing.For("Protocol").Error(this, "Failed to read HTTP/1.0 response body: {0}", failed.Reason.Message); - _deferredContext = null; + _deferredFeatures = null; + _errorOccurred = true; } break; } } + private void EncodeDeferredResponse(ReadOnlySpan body) + { + if (_deferredFeatures is null) + { + return; + } + + TransportBuffer? item = null; + try + { + var bufferSize = 8192 + body.Length; + item = TransportBuffer.Rent(bufferSize); + var written = _encoder.EncodeDeferred(item.FullMemory.Span, _deferredFeatures, body); + item.Length = written; + + _ops.OnOutbound(new TransportData(item)); + } + catch (Exception ex) + { + item?.Dispose(); + + Tracing.For("Protocol").Error(this, "Failed to encode HTTP/1.0 response: {0}", ex.Message); + } + finally + { + _deferredFeatures = null; + } + } + public void Cleanup() { _activeBodyEncoder?.Dispose(); _activeBodyEncoder = null; _deferredBodyOwner?.Dispose(); _deferredBodyOwner = null; - _deferredContext = null; + _deferredFeatures = null; } } diff --git a/src/TurboHTTP/Protocol/Syntax/Http11/Client/HeaderBuilder.cs b/src/TurboHTTP/Protocol/Syntax/Http11/Client/HeaderBuilder.cs index 15c03949e..2d5a33a70 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http11/Client/HeaderBuilder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http11/Client/HeaderBuilder.cs @@ -10,10 +10,17 @@ internal static class HeaderBuilder public static HeaderCollection Build(HttpRequestMessage request, Http11ClientEncoderOptions options) { var collection = new HeaderCollection(); + Build(request, options, collection); + return collection; + } + + public static void Build(HttpRequestMessage request, Http11ClientEncoderOptions options, HeaderCollection target) + { + target.Clear(); if (options.AutoHost) { - AddHostHeader(collection, request.RequestUri!); + AddHostHeader(target, request.RequestUri!); } var isChunked = request.Headers.TransferEncodingChunked == true; @@ -25,19 +32,17 @@ public static HeaderCollection Build(HttpRequestMessage request, Http11ClientEnc if (options.AutoAcceptEncoding) { - AddAcceptEncodingIfNeeded(collection, request.Headers); + AddAcceptEncodingIfNeeded(target, request.Headers); } - AddHeaders(collection, request.Headers, skipHost: true); + AddHeaders(target, request.Headers, skipHost: true); if (request.Content != null) { - AddContentHeaders(collection, request.Content.Headers, isChunked); + AddContentHeaders(target, request.Content.Headers, isChunked); } - AddConnectionHeader(collection, request.Headers); - - return collection; + AddConnectionHeader(target, request.Headers); } private static void AddHostHeader(HeaderCollection collection, Uri uri) diff --git a/src/TurboHTTP/Protocol/Syntax/Http11/Client/Http11ClientEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http11/Client/Http11ClientEncoder.cs index a7ab77895..c0806a726 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http11/Client/Http11ClientEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http11/Client/Http11ClientEncoder.cs @@ -1,6 +1,7 @@ using Akka.Actor; using TurboHTTP.Protocol.LineBased; using TurboHTTP.Protocol.LineBased.Body; +using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http11.Options; namespace TurboHTTP.Protocol.Syntax.Http11.Client; @@ -8,6 +9,7 @@ namespace TurboHTTP.Protocol.Syntax.Http11.Client; internal sealed class Http11ClientEncoder { private readonly Http11ClientEncoderOptions _options; + private readonly HeaderCollection _reusableHeaders = new(); public Http11ClientEncoder(Http11ClientEncoderOptions options) { @@ -29,8 +31,8 @@ public int Encode(Span destination, HttpRequestMessage request, IActorRef var writer = SpanWriter.Create(destination); var targetStr = request.ResolveTarget(); RequestLineWriter.Write(ref writer, request.Method.Method, targetStr, request.Version); - var headers = HeaderBuilder.Build(request, _options); - HeaderBlockWriter.Write(ref writer, headers); + HeaderBuilder.Build(request, _options, _reusableHeaders); + HeaderBlockWriter.Write(ref writer, _reusableHeaders); bodyEncoder?.Start(bodyStream!, stageActor); diff --git a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerDecoder.cs b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerDecoder.cs index f5af36750..889454a52 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerDecoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerDecoder.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.LineBased; using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http11.Options; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http11.Server; diff --git a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs index 59ad5f5b8..550513b5e 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerEncoder.cs @@ -1,10 +1,9 @@ using System.Net; -using Akka.Actor; +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.LineBased; using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http11.Options; -using TurboHTTP.Server; namespace TurboHTTP.Protocol.Syntax.Http11.Server; @@ -32,38 +31,46 @@ public void CancelActiveBody() _activeBodyEncoder = null; } - public int Encode(Span destination, TurboHttpContext context, bool isChunked = false, bool connectionClose = false) + public int Encode(Span destination, IFeatureCollection features, bool isChunked = false, bool connectionClose = false) { var writer = SpanWriter.Create(destination); - StatusLineWriter.Write(ref writer, HttpVersion.Version11, context.Response.StatusCode); + var responseFeature = features.Get(); + var statusCode = responseFeature?.StatusCode ?? 500; + StatusLineWriter.Write(ref writer, HttpVersion.Version11, statusCode); _reusableHeaders.Clear(); var headers = _reusableHeaders; - foreach (var h in context.Response.Headers) + var responseHeaders = responseFeature?.Headers; + if (responseHeaders is not null) { - if (ConnectionSemantics.IsHopByHop(h.Key)) + foreach (var h in responseHeaders) { - continue; - } + if (ConnectionSemantics.IsHopByHop(h.Key)) + { + continue; + } - foreach (var v in h.Value) - { - if (v is not null) + foreach (var v in h.Value) { - headers.Add(h.Key, v); + if (v is not null) + { + headers.Add(h.Key, v); + } } } } if (isChunked) { - headers.Add(WellKnownHeaders.TransferEncoding, WellKnownHeaders.ChunkedValue); + if (!headers.Contains(WellKnownHeaders.TransferEncoding)) + { + headers.Add(WellKnownHeaders.TransferEncoding, WellKnownHeaders.ChunkedValue); + } } - else + else if (!headers.Contains(WellKnownHeaders.ContentLength)) { - var contentLength = context.Response.ContentLength ?? 0L; - headers.Add(WellKnownHeaders.ContentLength, ContentLengthCache.GetValue(contentLength)); + headers.Add(WellKnownHeaders.ContentLength, ContentLengthCache.GetValue(0L)); } if (_options.WriteDateHeader && !headers.Contains(WellKnownHeaders.Date)) @@ -78,7 +85,7 @@ public int Encode(Span destination, TurboHttpContext context, bool isChunk HeaderBlockWriter.Write(ref writer, headers); - // For TurboHttpContext, body encoding is handled separately via the BodySink + // Body encoding is handled separately via the BodySink return writer.BytesWritten; } } \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs index f4cbdb99f..9fb7c777f 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http11/Server/Http11ServerStateMachine.cs @@ -1,13 +1,13 @@ +using System.Net; using Akka.Event; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.LineBased.Body; using TurboHTTP.Protocol.Syntax.Http11.Options; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; -using TurboHTTP.Streams; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Streams.Stages.Server; -using HttpVersion = System.Net.HttpVersion; namespace TurboHTTP.Protocol.Syntax.Http11.Server; @@ -49,8 +49,8 @@ public Http11ServerStateMachine(TurboServerOptions options, IServerStageOperatio var encOpts = new Http11ServerEncoderOptions { Shared = shared, - KeepAliveTimeout = options.Http1.KeepAliveTimeout ?? options.KeepAliveTimeout, - RequestHeadersTimeout = options.Http1.RequestHeadersTimeout ?? options.RequestHeadersTimeout, + KeepAliveTimeout = options.Http1.KeepAliveTimeout ?? options.Limits.KeepAliveTimeout, + RequestHeadersTimeout = options.Http1.RequestHeadersTimeout ?? options.Limits.RequestHeadersTimeout, }; var decOpts = new Http11ServerDecoderOptions @@ -135,21 +135,22 @@ public void DecodeClientData(ITransportInbound data) var feature = _decoder.GetRequestFeature(); var hasBody = feature.Body != Stream.Null; - var context = ServerContextFactory.Create(feature, hasBody, _ops.Services, _ops.ConnectionInfo, _ops.TlsHandshakeFeature); + var features = FeatureCollectionFactory.Create(feature, hasBody, _ops.Services, _ops.ConnectionFeature, + _ops.TlsHandshakeFeature, _serverOptions.Limits.MaxRequestBodySize); if (!ShouldComplete && feature.Protocol == "HTTP/1.0") { ShouldComplete = true; } - if (TryHandleH2cUpgrade(context)) + if (TryHandleH2cUpgrade(features)) { _decoder.Reset(); break; } _pendingResponseCount++; - _ops.OnRequest(context); + _ops.OnRequest(features); _decoder.Reset(); } } @@ -163,7 +164,7 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) + public void OnResponse(IFeatureCollection features) { if (_pendingResponseCount == 0) { @@ -172,8 +173,8 @@ public void OnResponse(TurboHttpContext context) _pendingResponseCount--; - var responseFeature = context.Features.Get(); - var responseBody = context.Features.Get(); + var responseFeature = features.Get(); + var responseBody = features.Get(); var statusCode = responseFeature?.StatusCode ?? 200; var suppressBody = statusCode is >= 100 and < 200 or 204 or 304; @@ -186,7 +187,7 @@ public void OnResponse(TurboHttpContext context) var responseBuffer = TransportBuffer.Rent(8192); var span = responseBuffer.FullMemory.Span; - var written = _encoder.Encode(span, context, isChunked, connectionClose: ShouldComplete); + var written = _encoder.Encode(span, features, isChunked, connectionClose: ShouldComplete); responseBuffer.Length = written; _ops.OnOutbound(new TransportData(responseBuffer)); @@ -196,10 +197,11 @@ public void OnResponse(TurboHttpContext context) { _ops.OnScheduleTimer("keep-alive", _keepAliveTimeout); } + return; } - if (!_draining && _decoder.CurrentBodyDecoder is { } bodyDecoder && !bodyDecoder.IsComplete) + if (!_draining && _decoder.CurrentBodyDecoder is { IsComplete: false }) { _draining = true; } @@ -263,6 +265,7 @@ public void OnBodyMessage(object msg) { _ops.OnScheduleTimer("keep-alive", _keepAliveTimeout); } + break; case OutboundBodyFailed failed: @@ -272,7 +275,7 @@ public void OnBodyMessage(object msg) } } - private static long? ExtractContentLength(ITurboResponseFeature? responseFeature) + private static long? ExtractContentLength(IHttpResponseFeature? responseFeature) { if (responseFeature?.Headers is null) { @@ -293,14 +296,14 @@ public void OnBodyMessage(object msg) return null; } - private bool TryHandleH2cUpgrade(TurboHttpContext context) + private bool TryHandleH2cUpgrade(IFeatureCollection features) { if (_ops is not IProtocolSwitchCapable switchable) { return false; } - var requestFeature = context.Features.Get(); + var requestFeature = features.Get(); var requestHeaders = requestFeature?.Headers; if (requestHeaders is null) { @@ -308,8 +311,9 @@ private bool TryHandleH2cUpgrade(TurboHttpContext context) } var hasUpgrade = requestHeaders.TryGetValue("Upgrade", out var upgradeValue) - && !string.IsNullOrEmpty(upgradeValue) - && upgradeValue.ToString().Split(',').Any(v => v.Trim().Equals("h2c", StringComparison.OrdinalIgnoreCase)); + && !string.IsNullOrEmpty(upgradeValue) + && upgradeValue.ToString().Split(',') + .Any(v => v.Trim().Equals("h2c", StringComparison.OrdinalIgnoreCase)); if (!hasUpgrade) { @@ -327,8 +331,7 @@ private bool TryHandleH2cUpgrade(TurboHttpContext context) responseBuffer.Length = responseBytes.Length; _ops.OnOutbound(new TransportData(responseBuffer)); - switchable.RequestProtocolSwitch( - ops => new Http2ServerStateMachine(_serverOptions, ops)); + switchable.RequestProtocolSwitch(ops => new Http2ServerStateMachine(_serverOptions, ops)); return true; } @@ -343,6 +346,7 @@ public void Cleanup() _ops.OnCancelTimer("request-headers"); _requestHeadersTimerActive = false; } + _ops.OnCancelTimer("keep-alive"); } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientDecoder.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientDecoder.cs index 90d5acc7a..1a1c0adc7 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientDecoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientDecoder.cs @@ -18,6 +18,11 @@ internal sealed class Http2ClientDecoder( private HpackDecoder _hpack = new(); + public void SetMaxAllowedTableSize(int size) + { + _hpack.SetMaxAllowedTableSize(size); + } + public void ResetHpack() { _hpack = new HpackDecoder(); diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientSessionManager.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientSessionManager.cs index 95e290cfe..7e5ae2879 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientSessionManager.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Client/Http2ClientSessionManager.cs @@ -58,6 +58,7 @@ public Http2ClientSessionManager( 1000); _statePool = new StackStreamStatePool(poolCapacity, () => new StreamState()); _responseDecoder = new Http2ClientDecoder(); + _responseDecoder.SetMaxAllowedTableSize(encoderOptions.HeaderTableSize); } public TransportData? TryBuildPreface() @@ -571,15 +572,24 @@ private void HandleOutboundBodyChunk(StreamBodyChunk chunk) return; } - var window = _flow.GetSendWindow(streamId); + var window = (int)Math.Min(_flow.GetSendWindow(streamId), int.MaxValue); if (window >= chunk.Length) { - EmitDataFrames(streamId, chunk.Owner.Memory[..chunk.Length]); + EmitDataFrames(streamId, chunk.Data); _flow.OnDataSent(streamId, chunk.Length); chunk.Owner.Dispose(); return; } + if (window > 0) + { + EmitDataFrames(streamId, chunk.Data[..window]); + _flow.OnDataSent(streamId, window); + var remainder = chunk with { Offset = chunk.Offset + window, Length = chunk.Length - window }; + state.EnqueueBodyChunk(remainder); + return; + } + state.EnqueueBodyChunk(chunk); } @@ -615,16 +625,27 @@ private void DrainOutboundBuffer(int streamId) while (state.PeekBodyChunk() is { } next) { - var window = _flow.GetSendWindow(streamId); - if (window < next.Length) + var window = (int)Math.Min(_flow.GetSendWindow(streamId), int.MaxValue); + if (window <= 0) { break; } state.TryDequeueBodyChunk(out var chunk); - EmitDataFrames(streamId, chunk!.Owner.Memory[..chunk.Length]); - _flow.OnDataSent(streamId, chunk.Length); - chunk.Owner.Dispose(); + + if (window >= chunk!.Length) + { + EmitDataFrames(streamId, chunk.Data); + _flow.OnDataSent(streamId, chunk.Length); + chunk.Owner.Dispose(); + } + else + { + EmitDataFrames(streamId, chunk.Data[..window]); + _flow.OnDataSent(streamId, window); + state.PrependBodyChunk(chunk with { Offset = chunk.Offset + window, Length = chunk.Length - window }); + break; + } } if (state is { HasPendingOutbound: false, IsBodyEncoderComplete: true }) diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/FlowController.cs b/src/TurboHTTP/Protocol/Syntax/Http2/FlowController.cs index 7a790e51e..5eb316cd6 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/FlowController.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/FlowController.cs @@ -12,6 +12,8 @@ internal sealed class FlowController : IFlowController private int _recvConnectionWindow; private int _initialRecvStreamWindow; + public int RecvConnectionWindow => _recvConnectionWindow; + private long _connectionSendWindow; private long _initialSendStreamWindow; private readonly Dictionary _streamSendWindows = new(); diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Server/BodyRateState.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Server/BodyRateState.cs index ecb205bc5..e68e595fe 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Server/BodyRateState.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Server/BodyRateState.cs @@ -19,21 +19,15 @@ internal sealed class BodyRateState /// /// Timestamp (in milliseconds from Environment.TickCount64) of last rate check. /// - public long LastCheckTimestamp { get; set; } + public long LastCheckTimestamp { get; set; } = Environment.TickCount64; /// /// Timestamp (in milliseconds from Environment.TickCount64) when grace period started. /// - public long GracePeriodStartTimestamp { get; set; } + public long GracePeriodStartTimestamp { get; set; } = Environment.TickCount64; /// /// Whether the stream is currently in its grace period (allowed to have slow data rate). /// public bool InGracePeriod { get; set; } - - public BodyRateState() - { - LastCheckTimestamp = Environment.TickCount64; - GracePeriodStartTimestamp = Environment.TickCount64; - } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerDecoder.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerDecoder.cs index 4dc9d944d..4fdbcf354 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerDecoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerDecoder.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http2.Hpack; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http2.Server; diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs index 005f9ee61..e48894335 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerEncoder.cs @@ -1,9 +1,8 @@ using System.Buffers; using Microsoft.AspNetCore.Http; -using TurboHTTP.Context; +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http2.Hpack; -using TurboHTTP.Server; namespace TurboHTTP.Protocol.Syntax.Http2.Server; @@ -49,9 +48,9 @@ private void EncodeHeaderFrames(List frames, int streamId, ReadOnlyM } } - public IReadOnlyList EncodeHeaders(TurboHttpContext context, int streamId, bool hasBody) + public IReadOnlyList EncodeHeaders(IFeatureCollection features, int streamId, bool hasBody) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(features); if (streamId < 0) { @@ -61,7 +60,7 @@ public IReadOnlyList EncodeHeaders(TurboHttpContext context, int str ReturnRentedBuffers(); _reusableHeaders.Clear(); - BuildHeaderList(context, _reusableHeaders); + BuildHeaderList(features, _reusableHeaders); var hpackOwner = MemoryPool.Shared.Rent(4096); _rentedBodyOwners.Add(hpackOwner); @@ -75,17 +74,26 @@ public IReadOnlyList EncodeHeaders(TurboHttpContext context, int str return _reusableFrames; } - private static void BuildHeaderList(TurboHttpContext context, List headers) + private static void BuildHeaderList(IFeatureCollection features, List headers) { // RFC 9113 §7.2: :status pseudo-header (required) - headers.Add(new HpackHeader(WellKnownHeaders.Status, WellKnownHeaders.GetStatusCodeString(context.Response.StatusCode))); + var responseFeature = features.Get(); + var statusCode = responseFeature?.StatusCode ?? 500; + headers.Add(new HpackHeader(WellKnownHeaders.Status, + WellKnownHeaders.GetStatusCodeString(statusCode))); // Add regular headers - foreach (var h in context.Response.Headers) + var responseHeaders = responseFeature?.Headers; + if (responseHeaders is not null) { - if (!ContentHeaderClassifier.IsForbiddenConnectionHeader(h.Key)) + foreach (var h in responseHeaders) { - var value = h.Value.Count == 1 ? h.Value[0]! : string.Join(", ", h.Value); + if (ContentHeaderClassifier.IsForbiddenConnectionHeader(h.Key)) + { + continue; + } + + var value = h.Value.Count == 1 ? h.Value[0]! : string.Join(WellKnownHeaders.CommaSpace, h.Value); headers.Add(new HpackHeader(ContentHeaderClassifier.ToLowerAscii(h.Key), value)); } } @@ -116,7 +124,7 @@ public void ApplyClientSettings(IEnumerable<(SettingsParameter Key, uint Value)> /// RFC 9113 §8.1: Trailers are sent as a HEADERS frame with END_STREAM. /// RFC 9110 §6.5.1: Filters prohibited trailer fields (transfer-encoding, content-length, etc.). /// - public IReadOnlyList EncodeTrailers(int streamId, ITurboHeaderDictionary trailers) + public IReadOnlyList EncodeTrailers(int streamId, IHeaderDictionary trailers) { ArgumentNullException.ThrowIfNull(trailers); @@ -128,13 +136,14 @@ public IReadOnlyList EncodeTrailers(int streamId, ITurboHeaderDictio { if (TrailerFieldValidator.IsAllowedInTrailer(header.Key)) { - _reusableHeaders.Add(new HpackHeader(ContentHeaderClassifier.ToLowerAscii(header.Key), header.Value.ToString() ?? string.Empty)); + _reusableHeaders.Add(new HpackHeader(ContentHeaderClassifier.ToLowerAscii(header.Key), + header.Value.ToString())); } } if (_reusableHeaders.Count == 0) { - return Array.Empty(); + return []; } var hpackOwner = MemoryPool.Shared.Rent(4096); @@ -166,5 +175,4 @@ private void ReturnRentedBuffers() _rentedBodyOwners.Clear(); } - } \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs index f812d3086..ad8faf934 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerSessionManager.cs @@ -1,14 +1,11 @@ using System.Text; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context; -using TurboHTTP.Context.Features; -using TurboHTTP.Internal; using TurboHTTP.Protocol.Multiplexed; using TurboHTTP.Protocol.Multiplexed.Body; using TurboHTTP.Protocol.Syntax.Http2.Options; using TurboHTTP.Server; -using TurboHTTP.Streams; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Streams.Stages.Server; using static Servus.Core.Servus; @@ -35,6 +32,7 @@ internal sealed class Http2ServerSessionManager private int _nextContinuationStreamId; private bool _continuationEndStream; private readonly Dictionary _bodyRateStates = new(); + private bool _prefaceConsumed; public int ActiveStreamCount => _streams.Count; public int MaxConcurrentStreams => _decoderOptions.MaxConcurrentStreams; @@ -43,18 +41,17 @@ public Http2ServerSessionManager( Http2ServerEncoderOptions encoderOptions, Http2ServerDecoderOptions decoderOptions, IServerStageOperations ops, - int initialConnectionWindowSize = 65535, - int initialStreamWindowSize = 65535, - long maxRequestBodySize = 30 * 1024 * 1024) + TurboServerOptions options) { _encoderOptions = encoderOptions; _decoderOptions = decoderOptions; _ops = ops ?? throw new ArgumentNullException(nameof(ops)); - _requestDecoder = new Http2ServerDecoder(16 * 1024, 64 * 1024); - _flow = new FlowController(initialConnectionWindowSize, initialStreamWindowSize); + + _requestDecoder = new Http2ServerDecoder(options.Http2.HeaderTableSize, options.Http2.MaxHeaderListSize); + _flow = new FlowController(options.Http2.InitialConnectionWindowSize, options.Http2.InitialStreamWindowSize); _tracker = new StreamTracker(initialNextStreamId: 1, decoderOptions.MaxConcurrentStreams); - _maxRequestBodySize = maxRequestBodySize; - _initialStreamWindowSize = initialStreamWindowSize; + _maxRequestBodySize = options.Http2.MaxRequestBodySize; + _initialStreamWindowSize = options.Http2.InitialStreamWindowSize; var statePoolCapacity = Math.Min( decoderOptions.MaxConcurrentStreams > 0 ? decoderOptions.MaxConcurrentStreams : 100, @@ -76,10 +73,21 @@ public void PreStart() var settingsFrame = new SettingsFrame(settingsParams, isAck: false); EmitFrame(settingsFrame); + + var connectionWindowIncrement = _flow.RecvConnectionWindow - 65535; + if (connectionWindowIncrement > 0) + { + EmitFrame(new WindowUpdateFrame(0, connectionWindowIncrement)); + } } public void DecodeClientData(TransportBuffer buffer) { + if (!_prefaceConsumed) + { + SkipConnectionPreface(buffer); + } + var frames = _frameDecoder.Decode(buffer); for (var i = 0; i < frames.Count; i++) { @@ -87,6 +95,22 @@ public void DecodeClientData(TransportBuffer buffer) } } + private static ReadOnlySpan ConnectionPrefaceMagic => "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"u8; + + private void SkipConnectionPreface(TransportBuffer buffer) + { + _prefaceConsumed = true; + + var span = buffer.Memory.Span; + if (span.Length >= ConnectionPrefaceMagic.Length + && span[..ConnectionPrefaceMagic.Length].SequenceEqual(ConnectionPrefaceMagic)) + { + var remaining = span.Length - ConnectionPrefaceMagic.Length; + span[ConnectionPrefaceMagic.Length..].CopyTo(span); + buffer.Length = remaining; + } + } + private void ProcessFrame(Http2Frame frame) { switch (frame) @@ -125,22 +149,24 @@ private void ProcessFrame(Http2Frame frame) } } - public void OnResponse(TurboHttpContext context) + public void OnResponse(IFeatureCollection features) { - var streamId = GetStreamIdFromContext(context); + var streamId = GetStreamIdFromFeatures(features); if (!_streams.TryGetValue(streamId, out var state)) { Tracing.For("Protocol").Warning(this, "HTTP/2: Response for unknown stream {0}", streamId); return; } - state.SetTurboContext(context); + state.SetFeatures(features); - var responseFeature = context.Features.Get(); + var responseFeature = features.Get(); + var responseBody = features.Get(); var contentLength = ExtractContentLength(responseFeature); - var hasBody = contentLength is not 0; + var hasBody = contentLength is not null and not 0 + || (contentLength is null && responseBody is TurboHttpResponseBodyFeature { HasStarted: true }); - var frames = _responseEncoder.EncodeHeaders(context, streamId, hasBody); + var frames = _responseEncoder.EncodeHeaders(features, streamId, hasBody); for (var i = 0; i < frames.Count; i++) { EmitFrame(frames[i]); @@ -151,8 +177,6 @@ public void OnResponse(TurboHttpContext context) CloseStream(streamId); return; } - - var responseBody = context.Features.Get(); if (responseBody is not TurboHttpResponseBodyFeature turboBody) { CloseStream(streamId); @@ -171,7 +195,7 @@ public void OnResponse(TurboHttpContext context) state.StartBodyEncoder(bodyStream, streamId, _ops.StageActor); } - private static long? ExtractContentLength(ITurboResponseFeature? responseFeature) + private static long? ExtractContentLength(IHttpResponseFeature? responseFeature) { if (responseFeature?.Headers is null) { @@ -245,8 +269,8 @@ private void HandleOutboundBodyComplete(int streamId) if (!state.HasPendingOutbound) { - var context = state.GetTurboContext(); - var trailerFeature = context?.Features.Get(); + var features = state.GetFeatures(); + var trailerFeature = features?.Get(); var hasTrailers = trailerFeature?.Trailers.Count > 0; if (hasTrailers) @@ -257,13 +281,13 @@ private void HandleOutboundBodyComplete(int streamId) { EmitFrame(trailerFrames[i]); } - CloseStream(streamId); } else { EmitFrame(new DataFrame(streamId, ReadOnlyMemory.Empty, endStream: true)); - CloseStream(streamId); } + + CloseStream(streamId); } } @@ -290,8 +314,8 @@ public void DrainOutboundBuffer(int streamId) if (state is { HasPendingOutbound: false, IsBodyEncoderComplete: true }) { - var context = state.GetTurboContext(); - var trailerFeature = context?.Features.Get(); + var features = state.GetFeatures(); + var trailerFeature = features?.Get(); var hasTrailers = trailerFeature?.Trailers.Count > 0; if (hasTrailers) @@ -302,13 +326,13 @@ public void DrainOutboundBuffer(int streamId) { EmitFrame(trailerFrames[i]); } - CloseStream(streamId); } else { EmitFrame(new DataFrame(streamId, ReadOnlyMemory.Empty, endStream: true)); - CloseStream(streamId); } + + CloseStream(streamId); } } @@ -537,14 +561,15 @@ private void DecodeAndEmitRequest(int streamId, StreamState state, bool endStrea requestFeature.Body = state.GetBodyStream(); } - var context = ServerContextFactory.Create(requestFeature, hasBody, _ops.Services, _ops.ConnectionInfo, _ops.TlsHandshakeFeature); - context.Features.Set(new TurboStreamIdFeature(streamId)); + var features = FeatureCollectionFactory.Create(requestFeature, hasBody, _ops.Services, + _ops.ConnectionFeature, _ops.TlsHandshakeFeature, _maxRequestBodySize); + features.Set(new TurboStreamIdFeature(streamId)); var capturedStreamId = streamId; - context.Features.Set(new TurboHttpResetFeature( - errorCode => EmitRstStream(capturedStreamId, (Http2ErrorCode)errorCode))); + features.Set(new TurboHttpResetFeature(errorCode => + EmitRstStream(capturedStreamId, (Http2ErrorCode)errorCode))); - _ops.OnRequest(context); + _ops.OnRequest(features); } catch (HttpProtocolException ex) { @@ -554,9 +579,9 @@ private void DecodeAndEmitRequest(int streamId, StreamState state, bool endStrea } } - private int GetStreamIdFromContext(TurboHttpContext context) + private static int GetStreamIdFromFeatures(IFeatureCollection features) { - var streamIdFeature = context.Features.Get(); + var streamIdFeature = features.Get(); if (streamIdFeature is not null) { return (int)streamIdFeature.StreamId; diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs index 83a976c96..3ecf4c7a4 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/Server/Http2ServerStateMachine.cs @@ -1,7 +1,7 @@ +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http2.Options; using TurboHTTP.Server; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Server; namespace TurboHTTP.Protocol.Syntax.Http2.Server; @@ -56,9 +56,7 @@ public Http2ServerStateMachine(TurboServerOptions options, IServerStageOperation encoderOpts, decoderOpts, ops, - options.Http2.InitialConnectionWindowSize, - options.Http2.InitialStreamWindowSize, - options.Http2.MaxRequestBodySize); + options); _keepAliveTimeout = options.Http2.KeepAliveTimeout; _requestHeadersTimeout = options.Http2.RequestHeadersTimeout; @@ -98,7 +96,7 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) => _sessionManager.OnResponse(context); + public void OnResponse(IFeatureCollection features) => _sessionManager.OnResponse(features); public void OnDownstreamFinished() { diff --git a/src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs b/src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs index 5e571ced2..a6bf03c26 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http2/StreamState.cs @@ -1,8 +1,8 @@ using System.Buffers; using Akka.Actor; -using TurboHTTP.Context.Features; +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.Multiplexed.Body; -using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http2; @@ -19,7 +19,7 @@ internal sealed class StreamState private int _headerLength; private HttpResponseMessage? _response; private TurboHttpRequestFeature? _requestFeature; - private TurboHttpContext? _turboContext; + private IFeatureCollection? _features; private List<(string Name, string Value)>? _contentHeaders; private Dictionary? _pseudoHeaders; private IBodyDecoder? _bodyDecoder; @@ -67,12 +67,12 @@ public void InitRequestFeature(TurboHttpRequestFeature feature) public TurboHttpRequestFeature? GetRequestFeature() => _requestFeature; - public void SetTurboContext(TurboHttpContext context) + public void SetFeatures(IFeatureCollection features) { - _turboContext = context; + _features = features; } - public TurboHttpContext? GetTurboContext() => _turboContext; + public IFeatureCollection? GetFeatures() => _features; public void AddPseudoHeader(string name, string value) { @@ -176,6 +176,18 @@ public void EnqueueBodyChunk(StreamBodyChunk chunk) _outboundBuffer.Enqueue(chunk); } + public void PrependBodyChunk(StreamBodyChunk chunk) + { + _outboundBuffer ??= new Queue>(); + var existing = _outboundBuffer.ToArray(); + _outboundBuffer.Clear(); + _outboundBuffer.Enqueue(chunk); + foreach (var item in existing) + { + _outboundBuffer.Enqueue(item); + } + } + public void MarkBodyEncoderComplete() { IsBodyEncoderComplete = true; @@ -212,7 +224,7 @@ public void Reset() _headerLength = 0; _response = null; _requestFeature = null; - _turboContext = null; + _features = null; _contentHeaders = null; _pseudoHeaders = null; _bodyDecoder?.Dispose(); diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientEncoder.cs index 5a5921256..f2decc350 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientEncoder.cs @@ -74,7 +74,7 @@ public IReadOnlyList Encode(HttpRequestMessage request) FieldValidator.Validate(_reusableHeaders); // QPACK encode directly into a MemoryPool-rented buffer - var qpackOwner = MemoryPool.Shared.Rent(8192); + var qpackOwner = MemoryPool.Shared.Rent(4 * 1024); _rentedOwners.Add(qpackOwner); var qpackWriter = SpanWriter.Create(qpackOwner.Memory.Span); var qpackBytesWritten = _tableSync.Encoder.Encode(_reusableHeaders, ref qpackWriter); @@ -110,7 +110,7 @@ public IReadOnlyList Encode(HttpRequestMessage request) ValidatePseudoHeaders(_reusableHeaders); FieldValidator.Validate(_reusableHeaders); - var owner = MemoryPool.Shared.Rent(8192); + var owner = MemoryPool.Shared.Rent(4 * 1024); var w = SpanWriter.Create(owner.Memory.Span); var n = _tableSync.Encoder.Encode(_reusableHeaders, ref w); return (owner, n); diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientSessionManager.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientSessionManager.cs index d1d6d78c5..7a3c5ca90 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientSessionManager.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientSessionManager.cs @@ -110,10 +110,7 @@ public void EncodeRequest(HttpRequestMessage request) _qpackStreamManager.FlushEncoderInstructions(); - foreach (var frame in frames) - { - EmitSerializedFrame(frame, streamId); - } + EmitBatchedFrames(frames, streamId); if (request.Content is null) { @@ -316,6 +313,40 @@ private void FlushPreConnectBuffer() _preConnectBuffer.Clear(); } + private void EmitBatchedFrames(IReadOnlyList frames, long streamId) + { + if (frames.Count == 0) + { + return; + } + + if (frames.Count == 1) + { + EmitSerializedFrame(frames[0], streamId); + return; + } + + var totalSize = 0; + for (var i = 0; i < frames.Count; i++) + { + totalSize += frames[i].SerializedSize; + } + + var buf = TransportBuffer.Rent(totalSize); + var span = buf.FullMemory.Span; + var offset = 0; + + for (var i = 0; i < frames.Count; i++) + { + var frameSpan = span[offset..]; + var written = frames[i].WriteTo(ref frameSpan); + offset += written; + } + + buf.Length = offset; + EmitOutbound(new MultiplexedData(buf, streamId)); + } + private void EmitSerializedFrame(Http3Frame frame, long streamId) { var buf = TransportBuffer.Rent(frame.SerializedSize); diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientStateMachine.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientStateMachine.cs index 7006f413f..dcb776518 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientStateMachine.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Client/Http3ClientStateMachine.cs @@ -130,7 +130,7 @@ public void DecodeServerData(ITransportInbound data) return; } - case StreamOpened: + case StreamOpened { Id: var openedId }: { return; } @@ -141,7 +141,7 @@ public void DecodeServerData(ITransportInbound data) return; } - case StreamReadCompleted: + case StreamReadCompleted { Id: var srcId }: { return; } diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Client/StreamManager.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Client/StreamManager.cs index 70c574206..9029257f1 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Client/StreamManager.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Client/StreamManager.cs @@ -16,8 +16,8 @@ namespace TurboHTTP.Protocol.Syntax.Http3.Client; /// internal sealed class StreamManager { - private const int MaxPoolSize = 16; - private const int MaxDecoderPoolSize = 16; + private const int MaxPoolSize = 256; + private const int MaxDecoderPoolSize = 256; private readonly IClientStageOperations _ops; private readonly Http3ClientDecoder _responseDecoder; diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/BodyRateState.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/BodyRateState.cs index fcaab209a..9c0541454 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/BodyRateState.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/BodyRateState.cs @@ -19,21 +19,15 @@ internal sealed class BodyRateState /// /// Timestamp (in milliseconds from Environment.TickCount64) of last rate check. /// - public long LastCheckTimestamp { get; set; } + public long LastCheckTimestamp { get; set; } = Environment.TickCount64; /// /// Timestamp (in milliseconds from Environment.TickCount64) when grace period started. /// - public long GracePeriodStartTimestamp { get; set; } + public long GracePeriodStartTimestamp { get; set; } = Environment.TickCount64; /// /// Whether the stream is currently in its grace period (allowed to have slow data rate). /// public bool InGracePeriod { get; set; } - - public BodyRateState() - { - LastCheckTimestamp = Environment.TickCount64; - GracePeriodStartTimestamp = Environment.TickCount64; - } -} +} \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerDecoder.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerDecoder.cs index ce10d38f9..e153c98ec 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerDecoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerDecoder.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Semantics; using TurboHTTP.Protocol.Syntax.Http3.Qpack; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http3.Server; diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs index 019fe778a..d8007ee60 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerEncoder.cs @@ -1,5 +1,5 @@ +using Microsoft.AspNetCore.Http.Features; using TurboHTTP.Protocol.Syntax.Http3.Qpack; -using TurboHTTP.Server; namespace TurboHTTP.Protocol.Syntax.Http3.Server; @@ -30,32 +30,39 @@ public Http3ServerEncoder(QpackTableSync tableSync) /// Encodes a response to HTTP/3 HEADERS frame only. /// Body is handled asynchronously via IBodyEncoder and StreamState outbound buffer. /// - public HeadersFrame EncodeHeaders(TurboHttpContext context) + public HeadersFrame EncodeHeaders(IFeatureCollection features) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(features); _reusableHeaders.Clear(); - BuildHeaderList(context, _reusableHeaders); + BuildHeaderList(features, _reusableHeaders); var headerBlock = _tableSync.Encoder.Encode(_reusableHeaders); return new HeadersFrame(headerBlock); } - private static void BuildHeaderList(TurboHttpContext context, List<(string Name, string Value)> headers) + private static void BuildHeaderList(IFeatureCollection features, List<(string Name, string Value)> headers) { // RFC 9114 §6.3: :status pseudo-header (required, must be first) - headers.Add((WellKnownHeaders.Status, WellKnownHeaders.GetStatusCodeString(context.Response.StatusCode))); + var responseFeature = features.Get(); + var statusCode = responseFeature?.StatusCode ?? 500; + headers.Add((WellKnownHeaders.Status, WellKnownHeaders.GetStatusCodeString(statusCode))); // Add regular headers (lowercase per RFC 9114) - foreach (var h in context.Response.Headers) + var responseHeaders = responseFeature?.Headers; + if (responseHeaders is not null) { - if (!ContentHeaderClassifier.IsForbiddenConnectionHeader(h.Key)) + foreach (var h in responseHeaders) { + if (ContentHeaderClassifier.IsForbiddenConnectionHeader(h.Key)) + { + continue; + } + var value = h.Value.Count == 1 ? h.Value[0]! : string.Join(", ", h.Value); headers.Add((ContentHeaderClassifier.ToLowerAscii(h.Key), value)); } } } - } \ No newline at end of file diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs index 9b187fc74..d4fc1b4e0 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerSessionManager.cs @@ -1,14 +1,12 @@ using System.Buffers; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Multiplexed; using TurboHTTP.Protocol.Multiplexed.Body; using TurboHTTP.Protocol.Syntax.Http3.Options; using TurboHTTP.Protocol.Syntax.Http3.Qpack; using TurboHTTP.Server; -using TurboHTTP.Streams; +using TurboHTTP.Server.Context.Features; using TurboHTTP.Streams.Stages.Server; using static Servus.Core.Servus; @@ -112,9 +110,9 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) + public void OnResponse(IFeatureCollection features) { - var streamId = GetStreamIdFromContext(context); + var streamId = GetStreamIdFromFeatures(features); if (streamId < 0) { @@ -130,20 +128,21 @@ public void OnResponse(TurboHttpContext context) var (_, state) = streamData; - var headersFrame = _responseEncoder.EncodeHeaders(context); + var headersFrame = _responseEncoder.EncodeHeaders(features); EmitDataFrame(headersFrame, streamId); - var responseFeature = context.Features.Get(); + var responseFeature = features.Get(); + var responseBody = features.Get(); var contentLength = ExtractContentLength(responseFeature); - var hasBody = contentLength is not 0; + var hasStarted = responseBody is TurboHttpResponseBodyFeature { HasStarted: true }; + var hasBody = contentLength is not null and not 0 + || (contentLength is null && hasStarted); if (!hasBody) { _ops.OnOutbound(new CompleteWrites(streamId)); return; } - - var responseBody = context.Features.Get(); if (responseBody is not TurboHttpResponseBodyFeature turboBody) { _ops.OnOutbound(new CompleteWrites(streamId)); @@ -163,7 +162,7 @@ public void OnResponse(TurboHttpContext context) _ops.OnScheduleTimer(string.Concat("drain-body:", streamId.ToString()), TimeSpan.FromMilliseconds(0)); } - private static long? ExtractContentLength(ITurboResponseFeature? responseFeature) + private static long? ExtractContentLength(IHttpResponseFeature? responseFeature) { if (responseFeature?.Headers is null) { @@ -462,15 +461,15 @@ private void FlushPendingRequest(long streamId) requestFeature.Body = state.GetBodyStream(); } - var context = ServerContextFactory.Create(requestFeature, hasBody, _ops.Services, _ops.ConnectionInfo, _ops.TlsHandshakeFeature); - context.Features.Set(new TurboStreamIdFeature(streamId)); + var features = FeatureCollectionFactory.Create(requestFeature, hasBody, _ops.Services, _ops.ConnectionFeature, _ops.TlsHandshakeFeature, _maxRequestBodySize); + features.Set(new TurboStreamIdFeature(streamId)); var capturedStreamId = streamId; - context.Features.Set(new TurboHttpResetFeature( + features.Set(new TurboHttpResetFeature( errorCode => EmitRstStream(capturedStreamId, (ErrorCode)errorCode))); _bodyRateStates.Remove(streamId); - _ops.OnRequest(context); + _ops.OnRequest(features); } } @@ -504,9 +503,9 @@ private void HandleDataFrame(DataFrame dataFrame, long streamId, StreamState sta } } - private long GetStreamIdFromContext(TurboHttpContext context) + private long GetStreamIdFromFeatures(IFeatureCollection features) { - var streamIdFeature = context.Features.Get(); + var streamIdFeature = features.Get(); if (streamIdFeature is not null) { return streamIdFeature.StreamId; diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs index db6b64359..1bd96affb 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/Http3ServerStateMachine.cs @@ -1,7 +1,7 @@ +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http3.Options; using TurboHTTP.Server; -using TurboHTTP.Streams; using TurboHTTP.Streams.Stages.Server; namespace TurboHTTP.Protocol.Syntax.Http3.Server; @@ -86,9 +86,9 @@ public void DecodeClientData(ITransportInbound data) } } - public void OnResponse(TurboHttpContext context) + public void OnResponse(IFeatureCollection features) { - _sessionManager.OnResponse(context); + _sessionManager.OnResponse(features); } public void OnDownstreamFinished() diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/Server/ServerStreamResolver.cs b/src/TurboHTTP/Protocol/Syntax/Http3/Server/ServerStreamResolver.cs index 3e107db1d..2f67b6b4a 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/Server/ServerStreamResolver.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/Server/ServerStreamResolver.cs @@ -16,7 +16,7 @@ internal sealed class ServerStreamResolver public void OnServerStreamOpened(long quicStreamId) { - if (quicStreamId < 0 || (quicStreamId & 1) == 0) + if (quicStreamId < 0 || (quicStreamId & 0x02) == 0) { return; } diff --git a/src/TurboHTTP/Protocol/Syntax/Http3/StreamState.cs b/src/TurboHTTP/Protocol/Syntax/Http3/StreamState.cs index 2794de0eb..5667b6a98 100644 --- a/src/TurboHTTP/Protocol/Syntax/Http3/StreamState.cs +++ b/src/TurboHTTP/Protocol/Syntax/Http3/StreamState.cs @@ -1,6 +1,6 @@ using Akka.Actor; -using TurboHTTP.Context.Features; using TurboHTTP.Protocol.Multiplexed.Body; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Protocol.Syntax.Http3; diff --git a/src/TurboHTTP/Protocol/WellKnownHeaders.cs b/src/TurboHTTP/Protocol/WellKnownHeaders.cs index 7d512f8bf..bef404f60 100644 --- a/src/TurboHTTP/Protocol/WellKnownHeaders.cs +++ b/src/TurboHTTP/Protocol/WellKnownHeaders.cs @@ -52,6 +52,7 @@ internal static class WellKnownHeaders { public static readonly WellKnownHeader Colon = new(":"); public static readonly WellKnownHeader Comma = new(","); + public static readonly WellKnownHeader SemiColon = new(";"); public static readonly WellKnownHeader Space = new(" "); public static readonly WellKnownHeader Crlf = new("\r\n"); @@ -235,6 +236,7 @@ private static string[] BuildStatusCodeStrings() public static readonly WellKnownHeader ColonSpace = Colon + Space; public static readonly WellKnownHeader CommaSpace = Comma + Space; + public static readonly WellKnownHeader SemiColonSpace = SemiColon + Space; public static bool TryResolve(ReadOnlySpan bytes, [NotNullWhen(true)] out string? result) { diff --git a/src/TurboHTTP/Routing/AllowAnonymousMarker.cs b/src/TurboHTTP/Routing/AllowAnonymousMarker.cs deleted file mode 100644 index adce25b1a..000000000 --- a/src/TurboHTTP/Routing/AllowAnonymousMarker.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace TurboHTTP.Routing; - -internal sealed record AllowAnonymousMarker : IAllowAnonymous; diff --git a/src/TurboHTTP/Routing/AuthorizeData.cs b/src/TurboHTTP/Routing/AuthorizeData.cs deleted file mode 100644 index 8af42be39..000000000 --- a/src/TurboHTTP/Routing/AuthorizeData.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TurboHTTP.Routing; - -public sealed record AuthorizeData( - string? Policy, - string? Roles, - string? AuthenticationSchemes) : IAuthorizeData; diff --git a/src/TurboHTTP/Routing/Binding/DelegateHandlerBinder.cs b/src/TurboHTTP/Routing/Binding/DelegateHandlerBinder.cs deleted file mode 100644 index a2048d582..000000000 --- a/src/TurboHTTP/Routing/Binding/DelegateHandlerBinder.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System.Reflection; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal sealed class BindingValidationException(int statusCode, Dictionary>? errors = null) - : Exception("Parameter validation failed.") -{ - public int StatusCode { get; } = statusCode; - public Dictionary> Errors { get; } = errors ?? new Dictionary>(); -} - -internal static class DelegateHandlerBinder -{ - internal static Func> BindEntityDelegate( - string pattern, - Delegate? handler) - { - var method = handler!.Method; - var parameters = method.GetParameters(); - var routeSegments = ExtractRouteSegments(pattern); - - var binders = new ParameterBinder[parameters.Length]; - var requiresValidation = new bool[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - binders[i] = CreateBinder(parameters[i], routeSegments); - if (binders[i] is JsonBodyBinder or FormBinder) - { - requiresValidation[i] = ParameterValidator.HasValidationAttributes(parameters[i].ParameterType); - } - else if (binders[i] is AsParametersBinder asParams) - { - requiresValidation[i] = asParams.RequiresValidation; - } - } - - ValidateBinderConfiguration(pattern, parameters); - - return async (ctx, services) => - { - try - { - var args = await BindArgs(binders, ctx, services); - - var validationErrors = RunValidation(requiresValidation, args, parameters); - if (validationErrors is not null) - { - throw new BindingValidationException(400, validationErrors); - } - - var result = handler.DynamicInvoke(args); - if (result is Task taskObj) - { - return await taskObj; - } - - if (result is ValueTask vtObj) - { - return await vtObj; - } - - return result ?? throw new InvalidOperationException("Entity message factory returned null."); - } - catch (ParameterParseException) - { - throw new BindingValidationException(400); - } - }; - } - - internal static Func Bind( - string pattern, - Delegate handler) - { - var method = handler.Method; - var parameters = method.GetParameters(); - var routeSegments = ExtractRouteSegments(pattern); - - var binders = new ParameterBinder[parameters.Length]; - var requiresValidation = new bool[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - binders[i] = CreateBinder(parameters[i], routeSegments); - if (binders[i] is JsonBodyBinder or FormBinder) - { - requiresValidation[i] = ParameterValidator.HasValidationAttributes(parameters[i].ParameterType); - } - else if (binders[i] is AsParametersBinder asParams) - { - requiresValidation[i] = asParams.RequiresValidation; - } - } - - ValidateBinderConfiguration(pattern, parameters); - - var returnType = method.ReturnType; - var unwrappedType = returnType; - - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - unwrappedType = returnType.GetGenericArguments()[0]; - } - - if (typeof(IResult).IsAssignableFrom(unwrappedType)) - { - return CreateIResultHandler(handler, binders, returnType, requiresValidation, parameters); - } - - throw new InvalidOperationException( - string.Concat( - "Handler for '", pattern, - "' must return IResult or Task. Got: ", - returnType.Name)); - } - - private static Func CreateIResultHandler( - Delegate handler, ParameterBinder[] binders, Type returnType, bool[] requiresValidation, - ParameterInfo[] parameters) - { - return async (ctx, services) => - { - try - { - var args = await BindArgs(binders, ctx, services); - - var validationErrors = RunValidation(requiresValidation, args, parameters); - if (validationErrors is not null) - { - await ParameterValidator.WriteValidationError(ctx, validationErrors); - return; - } - - var result = handler.DynamicInvoke(args); - - IResult? iresult = null; - if (result is Task task) - { - await task; - if (returnType.IsGenericType) - { - iresult = task.GetType().GetProperty("Result")!.GetValue(task) as IResult; - } - } - else - { - iresult = result as IResult; - } - - if (iresult is null) - { - ctx.Response.StatusCode = 500; - return; - } - - await iresult.ExecuteAsync(ctx); - } - catch (ParameterParseException) - { - ctx.Response.StatusCode = 400; - } - }; - } - - private static Dictionary>? RunValidation( - bool[] requiresValidation, - object?[] args, - ParameterInfo[] parameters) - { - Dictionary>? allErrors = null; - - for (var i = 0; i < args.Length; i++) - { - if (!requiresValidation[i] || args[i] is null) - { - continue; - } - - var result = ParameterValidator.ValidateObject(args[i]!, parameters[i].Name!); - if (!result.IsValid) - { - allErrors ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var kv in result.Errors) - { - allErrors[kv.Key] = kv.Value; - } - } - } - - return allErrors; - } - - private static async ValueTask BindArgs(ParameterBinder[] binders, TurboHttpContext ctx, - IServiceProvider services) - { - var args = new object?[binders.Length]; - for (var i = 0; i < binders.Length; i++) - { - args[i] = await binders[i].BindAsync(ctx, services); - } - - return args; - } - - private static void ValidateBinderConfiguration(string pattern, ParameterInfo[] parameters) - { - var bodyCount = 0; - var hasForm = false; - var hasBody = false; - - for (var i = 0; i < parameters.Length; i++) - { - if (parameters[i].GetCustomAttribute() is not null) - { - bodyCount++; - hasBody = true; - } - - if (parameters[i].GetCustomAttribute() is not null) - { - hasForm = true; - } - } - - if (bodyCount > 1) - { - throw new InvalidOperationException( - string.Concat("Handler for '", pattern, "' has multiple [FromBody] parameters. Only one is allowed.")); - } - - if (hasBody && hasForm) - { - throw new InvalidOperationException( - string.Concat("Handler for '", pattern, - "' has both [FromBody] and [FromForm] parameters. These are mutually exclusive.")); - } - } - - private static ParameterBinder CreateBinder(ParameterInfo parameter, HashSet routeSegments) - { - var type = parameter.ParameterType; - var name = parameter.Name!; - - if (parameter.GetCustomAttribute() is { } fromRoute) - { - return new RouteValueBinder(fromRoute.Name ?? name, type); - } - - if (parameter.GetCustomAttribute() is { } fromQuery) - { - return new QueryStringBinder(fromQuery.Name ?? name, type); - } - - if (parameter.GetCustomAttribute() is { } fromHeader) - { - return new HeaderBinder(fromHeader.Name ?? name, type); - } - - if (parameter.GetCustomAttribute() is not null) - { - return new JsonBodyBinder(type); - } - - if (parameter.GetCustomAttribute() is { } fromForm) - { - if (type == typeof(IFormFile)) - { - return new FormFileBinder(fromForm.Name ?? name); - } - - return new FormBinder(fromForm.Name ?? name, type); - } - - if (parameter.GetCustomAttribute() is not null) - { - return new ServiceBinder(type); - } - - if (parameter.GetCustomAttribute() is not null) - { - return new AsParametersBinder(type, routeSegments); - } - - if (type == typeof(TurboHttpContext)) - { - return new ContextBinder(); - } - - if (type == typeof(CancellationToken)) - { - return new CancellationTokenBinder(); - } - - if (type == typeof(HttpContext)) - { - return new HttpContextBinder(); - } - - if (routeSegments.Contains(name)) - { - return new RouteValueBinder(name, type); - } - - if (type == typeof(IFormFile) || type == typeof(IFormFileCollection)) - { - return new FormFileBinder(name); - } - - if (type.IsInterface || (type.IsClass && type != typeof(string))) - { - return new ServiceBinder(type); - } - - return new QueryStringBinder(name, type); - } - - private static HashSet ExtractRouteSegments(string pattern) - { - var segments = new HashSet(StringComparer.OrdinalIgnoreCase); - var parts = pattern.Split('/'); - foreach (var part in parts) - { - if (part.StartsWith('{') && part.EndsWith('}')) - { - segments.Add(part[1..^1]); - } - } - - return segments; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/Binding/JsonBodyBinder.cs b/src/TurboHTTP/Routing/Binding/JsonBodyBinder.cs deleted file mode 100644 index 0ef1df196..000000000 --- a/src/TurboHTTP/Routing/Binding/JsonBodyBinder.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json; -using TurboHTTP.Context; -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal sealed class JsonBodyBinder(Type type) : ParameterBinder -{ - public override async ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var req = ctx.Request as TurboHttpRequest; - if (req?.Content is null) - { - return null; - } - - await using var stream = await req.Content.ReadAsStreamAsync(); - return await JsonSerializer.DeserializeAsync(stream, type); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/Binding/ParameterBinder.cs b/src/TurboHTTP/Routing/Binding/ParameterBinder.cs deleted file mode 100644 index 3b4af173b..000000000 --- a/src/TurboHTTP/Routing/Binding/ParameterBinder.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System.Globalization; -using System.Reflection; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal sealed class ParameterParseException(string message, Exception innerException) : Exception(message, innerException); - -internal abstract class ParameterBinder -{ - public abstract ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services); -} - -internal sealed class ContextBinder : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) => - ValueTask.FromResult(ctx); -} - -internal sealed class HttpContextBinder : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) => - ValueTask.FromResult(ctx); -} - -internal sealed class CancellationTokenBinder : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) => - ValueTask.FromResult(ctx.RequestAborted); -} - -internal sealed class RouteValueBinder(string name, Type type) : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - if (!ctx.Request.RouteValues.TryGetValue(name, out var value) || value is null) - { - return ValueTask.FromResult(type.IsValueType ? Activator.CreateInstance(type) : null); - } - - var str = value.ToString()!; - return ValueTask.FromResult(ParseValue(str, type)); - } - - internal static object ParseValue(string str, Type type) - { - try - { - if (type == typeof(string)) - { - return str; - } - - if (type == typeof(int)) - { - return int.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(long)) - { - return long.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(float)) - { - return float.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(double)) - { - return double.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(decimal)) - { - return decimal.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(bool)) - { - return bool.Parse(str); - } - - if (type == typeof(Guid)) - { - return Guid.Parse(str); - } - - if (type == typeof(DateTime)) - { - return DateTime.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(DateTimeOffset)) - { - return DateTimeOffset.Parse(str, CultureInfo.InvariantCulture); - } - - if (type == typeof(TimeSpan)) - { - return TimeSpan.Parse(str, CultureInfo.InvariantCulture); - } - - return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - throw new ParameterParseException( - string.Concat("Failed to parse '", str, "' as type '", type.Name, "'."), ex); - } - } -} - -internal sealed class HeaderBinder : ParameterBinder -{ - private readonly string _name; - private readonly Type _type; - - public HeaderBinder(string name, Type type) - { - _name = name; - _type = type; - } - - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var value = ctx.Request.Headers[_name].FirstOrDefault(); - if (value is null) - { - return ValueTask.FromResult( - _type.IsValueType ? Activator.CreateInstance(_type) : null); - } - - return ValueTask.FromResult(RouteValueBinder.ParseValue(value, _type)); - } -} - -internal sealed class FormBinder(string name, Type type) : ParameterBinder -{ - public override async ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var form = await ctx.Request.ReadFormAsync(ctx.RequestAborted); - var value = form[name].FirstOrDefault(); - if (value is null) - { - return type.IsValueType ? Activator.CreateInstance(type) : null; - } - - return RouteValueBinder.ParseValue(value, type); - } -} - -internal sealed class FormFileBinder : ParameterBinder -{ - private readonly string _name; - - public FormFileBinder(string name) - { - _name = name; - } - - public override async ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var form = await ctx.Request.ReadFormAsync(ctx.RequestAborted); - return form.Files.GetFile(_name); - } -} - -internal sealed class AsParametersBinder : ParameterBinder -{ - private readonly Type _type; - private readonly ConstructorBinder[] _ctorBinders; - - public AsParametersBinder(Type type, HashSet routeSegments, HashSet? visited = null) - { - _type = type; - visited ??= []; - - if (!visited.Add(type)) - { - throw new InvalidOperationException( - string.Concat("Circular [AsParameters] reference detected for type '", type.Name, "'.")); - } - - var ctor = type.GetConstructors() - .OrderByDescending(c => c.GetParameters().Length) - .FirstOrDefault() - ?? throw new InvalidOperationException( - string.Concat("[AsParameters] type '", type.Name, "' has no accessible constructor.")); - - var ctorParams = ctor.GetParameters(); - _ctorBinders = new ConstructorBinder[ctorParams.Length]; - - for (var i = 0; i < ctorParams.Length; i++) - { - var param = ctorParams[i]; - var matchingProp = type.GetProperties() - .FirstOrDefault(p => string.Equals(p.Name, param.Name, StringComparison.OrdinalIgnoreCase)); - - _ctorBinders[i] = new ConstructorBinder( - CreateBinderForMember(param, matchingProp, routeSegments, visited)); - } - - RequiresValidation = ParameterValidator.HasValidationAttributes(type); - } - - public override async ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var args = new object?[_ctorBinders.Length]; - for (var i = 0; i < _ctorBinders.Length; i++) - { - args[i] = await _ctorBinders[i].Binder.BindAsync(ctx, services); - } - - return Activator.CreateInstance(_type, args); - } - - internal bool RequiresValidation { get; } - - private static ParameterBinder CreateBinderForMember( - ParameterInfo param, - PropertyInfo? prop, - HashSet routeSegments, - HashSet visited) - { - // Collect attributes from both property and constructor parameter - var attrs = prop is not null - ? prop.GetCustomAttributes(true).Concat(param.GetCustomAttributes(true)).ToArray() - : param.GetCustomAttributes(true); - - var type = param.ParameterType; - var name = param.Name!; - - foreach (var attr in attrs) - { - if (attr is FromRouteAttribute fromRoute) - { - return new RouteValueBinder(fromRoute.Name ?? name, type); - } - - if (attr is FromQueryAttribute fromQuery) - { - return new QueryStringBinder(fromQuery.Name ?? name, type); - } - - if (attr is FromHeaderAttribute fromHeader) - { - return new HeaderBinder(fromHeader.Name ?? name, type); - } - - if (attr is FromBodyAttribute) - { - return new JsonBodyBinder(type); - } - - if (attr is FromFormAttribute fromForm) - { - if (type == typeof(IFormFile)) - { - return new FormFileBinder(fromForm.Name ?? name); - } - - return new FormBinder(fromForm.Name ?? name, type); - } - - if (attr is FromServicesAttribute) - { - return new ServiceBinder(type); - } - - if (attr is AsParametersAttribute) - { - return new AsParametersBinder(type, routeSegments, [..visited]); - } - } - - // Convention fallback - if (type == typeof(TurboHttpContext)) - { - return new ContextBinder(); - } - - if (type == typeof(CancellationToken)) - { - return new CancellationTokenBinder(); - } - - if (routeSegments.Contains(name)) - { - return new RouteValueBinder(name, type); - } - - if (type == typeof(IFormFile) || type == typeof(IFormFileCollection)) - { - return new FormFileBinder(name); - } - - if (type.IsInterface || (type.IsClass && type != typeof(string))) - { - return new ServiceBinder(type); - } - - return new QueryStringBinder(name, type); - } - - private sealed class ConstructorBinder(ParameterBinder binder) - { - public ParameterBinder Binder { get; } = binder; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/Binding/ParameterValidator.cs b/src/TurboHTTP/Routing/Binding/ParameterValidator.cs deleted file mode 100644 index d9a8db637..000000000 --- a/src/TurboHTTP/Routing/Binding/ParameterValidator.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json; -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal static class ParameterValidator -{ - public static ValidationResult ValidateObject(object value, string parameterName) - { - var context = new ValidationContext(value); - var results = new List(); - - if (Validator.TryValidateObject(value, context, results, validateAllProperties: true)) - { - return ValidationResult.Valid; - } - - var errors = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var r in results) - { - var memberNames = r.MemberNames.Any() ? r.MemberNames : [parameterName]; - foreach (var member in memberNames) - { - if (!errors.TryGetValue(member, out var list)) - { - list = []; - errors[member] = list; - } - - list.Add(r.ErrorMessage ?? "Validation failed."); - } - } - - return new ValidationResult(false, errors); - } - - public static bool HasValidationAttributes(Type type) - { - foreach (var prop in type.GetProperties()) - { - if (prop.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0) - { - return true; - } - } - - foreach (var param in type.GetConstructors() - .Where(c => c.IsPublic) - .SelectMany(c => c.GetParameters())) - { - if (param.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0) - { - return true; - } - } - - return false; - } - - public static async Task WriteValidationError(TurboHttpContext context, Dictionary> errors) - { - context.Response.StatusCode = 400; - context.Response.ContentType = "application/problem+json"; - - var problemDetails = new - { - type = "https://tools.ietf.org/html/rfc9110#section-15.5.1", - title = "Validation Failed", - status = 400, - errors - }; - - var json = JsonSerializer.Serialize(problemDetails); - var bytes = System.Text.Encoding.UTF8.GetBytes(json); - await context.Response.Body.WriteAsync(bytes); - } - - internal sealed class ValidationResult(bool isValid, Dictionary> errors) - { - public static readonly ValidationResult Valid = new(true, new Dictionary>()); - - public bool IsValid { get; } = isValid; - public Dictionary> Errors { get; } = errors; - } -} diff --git a/src/TurboHTTP/Routing/Binding/QueryStringBinder.cs b/src/TurboHTTP/Routing/Binding/QueryStringBinder.cs deleted file mode 100644 index 3e3a13cf7..000000000 --- a/src/TurboHTTP/Routing/Binding/QueryStringBinder.cs +++ /dev/null @@ -1,29 +0,0 @@ -using TurboHTTP.Context; -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal sealed class QueryStringBinder(string name, Type type) : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - { - var req = ctx.Request as TurboHttpRequest; - var uri = req?.RequestUri; - if (uri?.Query is not { Length: > 0 } query) - { - return ValueTask.FromResult(type.IsValueType ? Activator.CreateInstance(type) : null); - } - - var pairs = query.TrimStart('?').Split('&'); - foreach (var pair in pairs) - { - var kv = pair.Split('=', 2); - if (kv.Length == 2 && string.Equals(kv[0], name, StringComparison.OrdinalIgnoreCase)) - { - return ValueTask.FromResult(RouteValueBinder.ParseValue(Uri.UnescapeDataString(kv[1]), type)); - } - } - - return ValueTask.FromResult(type.IsValueType ? Activator.CreateInstance(type) : null); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/Binding/ServiceBinder.cs b/src/TurboHTTP/Routing/Binding/ServiceBinder.cs deleted file mode 100644 index 82061be20..000000000 --- a/src/TurboHTTP/Routing/Binding/ServiceBinder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Routing.Binding; - -internal sealed class ServiceBinder(Type serviceType) : ParameterBinder -{ - public override ValueTask BindAsync(TurboHttpContext ctx, IServiceProvider services) - => ValueTask.FromResult(services.GetService(serviceType)); -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/DelegateDispatcher.cs b/src/TurboHTTP/Routing/DelegateDispatcher.cs deleted file mode 100644 index 9e56186cc..000000000 --- a/src/TurboHTTP/Routing/DelegateDispatcher.cs +++ /dev/null @@ -1,11 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Routing; - -internal sealed class DelegateDispatcher(Func handler) : IRouteDispatcher -{ - public Task DispatchAsync(TurboHttpContext context, CancellationToken ct) - { - return handler(context); - } -} diff --git a/src/TurboHTTP/Routing/EndpointMetadata.cs b/src/TurboHTTP/Routing/EndpointMetadata.cs deleted file mode 100644 index b178e46b3..000000000 --- a/src/TurboHTTP/Routing/EndpointMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TurboHTTP.Routing; - -public sealed class EndpointMetadata -{ - public string? Name { get; internal set; } - public string? DisplayName { get; internal set; } - public List Tags { get; } = []; - public List Items { get; } = []; - public List AuthorizationPolicies { get; } = []; - public bool RequiresAuthorization { get; internal set; } - public bool AllowsAnonymous { get; internal set; } -} diff --git a/src/TurboHTTP/Routing/EntityDispatcher.cs b/src/TurboHTTP/Routing/EntityDispatcher.cs deleted file mode 100644 index b85391613..000000000 --- a/src/TurboHTTP/Routing/EntityDispatcher.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Akka.Actor; -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; - -namespace TurboHTTP.Routing; - -internal sealed class EntityDispatcher : IRouteDispatcher -{ - private readonly EntityMethodConfig _methodConfig; - private readonly EntityResponseMapperCollection _responseMappers; - private readonly TimeSpan _timeout; - private readonly IEntityActorResolver _resolver; - - public EntityDispatcher( - EntityMethodConfig methodConfig, - EntityResponseMapperCollection responseMappers, - TimeSpan timeout, - IEntityActorResolver resolver) - { - _methodConfig = methodConfig; - _responseMappers = responseMappers; - _timeout = timeout; - _resolver = resolver; - } - - public Task DispatchAsync(TurboHttpContext context, CancellationToken ct) - { - return _methodConfig.IsTell - ? ExecuteTell(context, ct) - : ExecuteAsk(context, ct); - } - - private async Task ExecuteAsk(TurboHttpContext ctx, CancellationToken ct) - { - try - { - var timeout = _methodConfig.TimeoutOverride ?? _timeout; - var actorRef = await ResolveActor(ctx.RequestServices, ct); - var message = await _methodConfig.MessageFactory(ctx, ctx.RequestServices); - var response = await actorRef.Ask(message, timeout, ct); - - var mapper = _methodConfig.EndpointMappers?.FindMapper(response.GetType()) - ?? _responseMappers.FindMapper(response.GetType()); - if (mapper is null) - { - ctx.Response.StatusCode = 500; - return; - } - - await mapper(ctx, response); - } - catch (BindingValidationException ex) - { - ctx.Response.StatusCode = ex.StatusCode; - if (ex.Errors.Count > 0) - { - await ParameterValidator.WriteValidationError(ctx, ex.Errors); - } - } - catch (TaskCanceledException) - { - ctx.Response.StatusCode = 504; - } - catch (AskTimeoutException) - { - ctx.Response.StatusCode = 504; - } - catch - { - ctx.Response.StatusCode = 500; - } - } - - private async Task ExecuteTell(TurboHttpContext ctx, CancellationToken cancellationToken) - { - try - { - var actorRef = await ResolveActor(ctx.RequestServices, cancellationToken); - var message = await _methodConfig.MessageFactory(ctx, ctx.RequestServices); - actorRef.Tell(message); - - if (_methodConfig.TellResponseHandler is not null) - { - await _methodConfig.TellResponseHandler(ctx); - } - else - { - ctx.Response.StatusCode = 202; - } - } - catch (BindingValidationException ex) - { - ctx.Response.StatusCode = ex.StatusCode; - if (ex.Errors.Count > 0) - { - await ParameterValidator.WriteValidationError(ctx, ex.Errors); - } - } - catch - { - ctx.Response.StatusCode = 503; - } - } - - private async ValueTask ResolveActor(IServiceProvider services, CancellationToken ct = default) - { - if (_resolver is null) - { - throw new InvalidOperationException("No resolver configured for entity actor"); - } - - return await _resolver.ResolveAsync(services, ct); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Routing/EntityMethodConfig.cs b/src/TurboHTTP/Routing/EntityMethodConfig.cs deleted file mode 100644 index 9fb79724a..000000000 --- a/src/TurboHTTP/Routing/EntityMethodConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Routing; - -internal sealed record EntityMethodConfig( - Func> MessageFactory, - bool IsTell, - TimeSpan? TimeoutOverride, - EntityResponseMapperCollection? EndpointMappers, - Func? TellResponseHandler); diff --git a/src/TurboHTTP/Routing/IAllowAnonymous.cs b/src/TurboHTTP/Routing/IAllowAnonymous.cs deleted file mode 100644 index 32a878ed4..000000000 --- a/src/TurboHTTP/Routing/IAllowAnonymous.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace TurboHTTP.Routing; - -public interface IAllowAnonymous; diff --git a/src/TurboHTTP/Routing/IAuthorizeData.cs b/src/TurboHTTP/Routing/IAuthorizeData.cs deleted file mode 100644 index 86f18de01..000000000 --- a/src/TurboHTTP/Routing/IAuthorizeData.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TurboHTTP.Routing; - -public interface IAuthorizeData -{ - string? Policy { get; } - string? Roles { get; } - string? AuthenticationSchemes { get; } -} diff --git a/src/TurboHTTP/Routing/IRouteDispatcher.cs b/src/TurboHTTP/Routing/IRouteDispatcher.cs deleted file mode 100644 index d5c90fa63..000000000 --- a/src/TurboHTTP/Routing/IRouteDispatcher.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TurboHTTP.Server; - -namespace TurboHTTP.Routing; - -internal interface IRouteDispatcher -{ - Task DispatchAsync(TurboHttpContext context, CancellationToken ct); -} diff --git a/src/TurboHTTP/Routing/ITagsMetadata.cs b/src/TurboHTTP/Routing/ITagsMetadata.cs deleted file mode 100644 index 571e0e5b9..000000000 --- a/src/TurboHTTP/Routing/ITagsMetadata.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TurboHTTP.Routing; - -public interface ITagsMetadata -{ - IReadOnlyList Tags { get; } -} diff --git a/src/TurboHTTP/Routing/RouteEntry.cs b/src/TurboHTTP/Routing/RouteEntry.cs deleted file mode 100644 index 33ff06323..000000000 --- a/src/TurboHTTP/Routing/RouteEntry.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.AspNetCore.Routing; - -namespace TurboHTTP.Routing; - -internal sealed class RouteEntry -{ - public string Method { get; } - public string Pattern { get; } - public string[] Segments { get; } - public IRouteDispatcher Dispatcher { get; } - public bool IsStatic { get; } - public TurboEndpointMetadata? Metadata { get; } - - public RouteEntry(string method, string pattern, IRouteDispatcher dispatcher, TurboEndpointMetadata? metadata = null) - { - Method = method; - Pattern = pattern; - Segments = pattern.Split('/', StringSplitOptions.RemoveEmptyEntries); - Dispatcher = dispatcher; - IsStatic = !Array.Exists(Segments, s => s.StartsWith('{')); - Metadata = metadata; - } - - public bool TryMatch(string method, ReadOnlySpan path, RouteValueDictionary routeValues) - { - if (Method != "*" && !string.Equals(Method, method, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - var pathSegments = SplitPath(path); - if (pathSegments.Length != Segments.Length) - { - return false; - } - - for (var i = 0; i < Segments.Length; i++) - { - var template = Segments[i]; - var actual = pathSegments[i]; - - if (template.StartsWith('{') && template.EndsWith('}')) - { - var paramName = template[1..^1]; - routeValues[paramName] = actual; - } - else if (!string.Equals(template, actual, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - return true; - } - - public bool IsStaticMatch(ReadOnlySpan path) - { - if (path.Length > 0 && path[0] == '/') - { - path = path[1..]; - } - - var segmentIndex = 0; - while (path.Length > 0) - { - if (segmentIndex >= Segments.Length) - { - return false; - } - - var template = Segments[segmentIndex]; - if (template.StartsWith('{')) - { - return false; - } - - int slashPos = path.IndexOf('/'); - var segment = slashPos < 0 ? path : path[..slashPos]; - - if (!segment.Equals(template, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - segmentIndex++; - path = slashPos < 0 ? default : path[(slashPos + 1)..]; - } - - return segmentIndex == Segments.Length; - } - - private static string[] SplitPath(ReadOnlySpan path) - { - if (path.Length > 0 && path[0] == '/') - { - path = path[1..]; - } - - if (path.Length == 0) - { - return []; - } - - return path.ToString().Split('/'); - } -} diff --git a/src/TurboHTTP/Routing/RouteTable.cs b/src/TurboHTTP/Routing/RouteTable.cs deleted file mode 100644 index 9f702c199..000000000 --- a/src/TurboHTTP/Routing/RouteTable.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Microsoft.AspNetCore.Routing; - -namespace TurboHTTP.Routing; - -public sealed class RouteMatchResult -{ - internal static readonly RouteValueDictionary EmptyRouteValues = new(); - public static readonly RouteMatchResult NoMatch = new(false, null, EmptyRouteValues); - - public bool IsMatch { get; } - internal IRouteDispatcher? Dispatcher { get; } - public RouteValueDictionary RouteValues { get; } - internal TurboEndpointMetadata? Metadata { get; } - - internal RouteMatchResult(bool isMatch, IRouteDispatcher? dispatcher, RouteValueDictionary routeValues, TurboEndpointMetadata? metadata = null) - { - IsMatch = isMatch; - Dispatcher = dispatcher; - RouteValues = routeValues; - Metadata = metadata; - } -} - -public sealed class RouteTable -{ - private sealed record DispatcherEntry(IRouteDispatcher Dispatcher, TurboEndpointMetadata? Metadata); - - private readonly Dictionary> _staticByPath; - private readonly Dictionary _parameterizedByMethod; - private readonly RouteEntry[] _wildcardParameterized; - - internal RouteTable(RouteEntry[] entries) - { - var staticByPath = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var paramByMethod = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var wildcardParam = new List(); - - foreach (var entry in entries) - { - if (entry.IsStatic) - { - if (!staticByPath.TryGetValue(entry.Pattern, out var methodMap)) - { - methodMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - staticByPath[entry.Pattern] = methodMap; - } - - var dispatcherEntry = new DispatcherEntry(entry.Dispatcher, entry.Metadata); - - if (entry.Method == "*") - { - methodMap.TryAdd("GET", dispatcherEntry); - methodMap.TryAdd("POST", dispatcherEntry); - methodMap.TryAdd("PUT", dispatcherEntry); - methodMap.TryAdd("DELETE", dispatcherEntry); - methodMap.TryAdd("PATCH", dispatcherEntry); - methodMap.TryAdd("HEAD", dispatcherEntry); - methodMap.TryAdd("OPTIONS", dispatcherEntry); - } - else - { - methodMap.TryAdd(entry.Method, dispatcherEntry); - } - } - else if (entry.Method == "*") - { - wildcardParam.Add(entry); - } - else - { - if (!paramByMethod.TryGetValue(entry.Method, out var list)) - { - list = []; - paramByMethod[entry.Method] = list; - } - list.Add(entry); - } - } - - _staticByPath = staticByPath; - _parameterizedByMethod = new Dictionary(paramByMethod.Count, StringComparer.OrdinalIgnoreCase); - foreach (var kv in paramByMethod) - { - _parameterizedByMethod[kv.Key] = kv.Value.ToArray(); - } - _wildcardParameterized = wildcardParam.ToArray(); - } - - public RouteMatchResult Match(string method, string path) - { - if (_staticByPath.TryGetValue(path, out var methodMap) - && methodMap.TryGetValue(method, out var dispatcherEntry)) - { - return new RouteMatchResult(true, dispatcherEntry.Dispatcher, RouteMatchResult.EmptyRouteValues, dispatcherEntry.Metadata); - } - - if (_parameterizedByMethod.TryGetValue(method, out var methodEntries)) - { - var routeValues = new RouteValueDictionary(); - foreach (var entry in methodEntries) - { - routeValues.Clear(); - if (entry.TryMatch(method, path, routeValues)) - { - return new RouteMatchResult(true, entry.Dispatcher, routeValues, entry.Metadata); - } - } - } - - { - var routeValues = new RouteValueDictionary(); - foreach (var entry in _wildcardParameterized) - { - routeValues.Clear(); - if (entry.TryMatch(method, path, routeValues)) - { - return new RouteMatchResult(true, entry.Dispatcher, routeValues, entry.Metadata); - } - } - } - - return RouteMatchResult.NoMatch; - } -} - -internal sealed class RouteTableBuilder -{ - private readonly List _entries = []; - - public RouteTableBuilder Add(string method, string pattern, IRouteDispatcher dispatcher) - { - _entries.Add(new RouteEntry(method, pattern, dispatcher)); - return this; - } - - public RouteTable Build() - { - return new RouteTable(_entries.ToArray()); - } -} diff --git a/src/TurboHTTP/Routing/TagsMetadata.cs b/src/TurboHTTP/Routing/TagsMetadata.cs deleted file mode 100644 index bcb9b0d78..000000000 --- a/src/TurboHTTP/Routing/TagsMetadata.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace TurboHTTP.Routing; - -public sealed record TagsMetadata(IReadOnlyList Tags) : ITagsMetadata; diff --git a/src/TurboHTTP/Routing/TurboEndpointMetadata.cs b/src/TurboHTTP/Routing/TurboEndpointMetadata.cs deleted file mode 100644 index e3bf31ffc..000000000 --- a/src/TurboHTTP/Routing/TurboEndpointMetadata.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace TurboHTTP.Routing; - -public sealed class TurboEndpointMetadata -{ - private readonly IReadOnlyList _items; - - public TurboEndpointMetadata(IReadOnlyList items) - { - _items = items; - } - - public IReadOnlyList Items => _items; - - public T? GetMetadata() where T : class - { - for (var i = _items.Count - 1; i >= 0; i--) - { - if (_items[i] is T match) - { - return match; - } - } - return null; - } - - public IEnumerable GetOrderedMetadata() where T : class - { - for (var i = 0; i < _items.Count; i++) - { - if (_items[i] is T match) - { - yield return match; - } - } - } - - public bool HasMetadata() where T : class - { - for (var i = 0; i < _items.Count; i++) - { - if (_items[i] is T) - { - return true; - } - } - return false; - } - - public static TurboEndpointMetadata Merge(TurboEndpointMetadata group, TurboEndpointMetadata route) - { - var merged = new List(group._items.Count + route._items.Count); - merged.AddRange(group._items); - merged.AddRange(route._items); - return new TurboEndpointMetadata(merged); - } -} diff --git a/src/TurboHTTP/Routing/TurboRouteTable.cs b/src/TurboHTTP/Routing/TurboRouteTable.cs deleted file mode 100644 index a2c0d16b9..000000000 --- a/src/TurboHTTP/Routing/TurboRouteTable.cs +++ /dev/null @@ -1,58 +0,0 @@ -using TurboHTTP.Server; -using TurboHTTP.Routing.Binding; - -namespace TurboHTTP.Routing; - -public sealed class TurboRouteTable -{ - private readonly List<(RouteEntry Entry, TurboRouteHandlerBuilder Builder)> _entries = []; - private RouteTable? _frozen; - - public TurboRouteHandlerBuilder Add(string method, string pattern, Func handler) - { - var dispatcher = new DelegateDispatcher(handler); - var builder = new TurboRouteHandlerBuilder(); - _entries.Add((new RouteEntry(method, pattern, dispatcher), builder)); - return builder; - } - - public TurboRouteHandlerBuilder Add(string method, string pattern, Delegate handler) - { - var bound = DelegateHandlerBinder.Bind(pattern, handler); - var dispatcher = new DelegateDispatcher((ctx) => bound(ctx, ctx.RequestServices)); - var builder = new TurboRouteHandlerBuilder(); - _entries.Add((new RouteEntry(method, pattern, dispatcher), builder)); - return builder; - } - - internal TurboRouteHandlerBuilder AddWithDispatcher(string method, string pattern, IRouteDispatcher dispatcher) - { - var builder = new TurboRouteHandlerBuilder(); - _entries.Add((new RouteEntry(method, pattern, dispatcher), builder)); - return builder; - } - - public TurboRouteGroupBuilder CreateGroup(string prefix) - { - return new TurboRouteGroupBuilder(prefix, this); - } - - internal RouteTable Freeze() - { - if (_frozen is not null) - { - return _frozen; - } - - var entriesWithMetadata = new RouteEntry[_entries.Count]; - for (var i = 0; i < _entries.Count; i++) - { - var (entry, builder) = _entries[i]; - var metadata = builder.BuildMetadata(); - entriesWithMetadata[i] = new RouteEntry(entry.Method, entry.Pattern, entry.Dispatcher, metadata); - } - - _frozen = new RouteTable(entriesWithMetadata); - return _frozen; - } -} diff --git a/src/TurboHTTP/Context/Features/IHttpStreamIdFeature.cs b/src/TurboHTTP/Server/Context/Features/IHttpStreamIdFeature.cs similarity index 81% rename from src/TurboHTTP/Context/Features/IHttpStreamIdFeature.cs rename to src/TurboHTTP/Server/Context/Features/IHttpStreamIdFeature.cs index 1beb1b2c4..62f3e6034 100644 --- a/src/TurboHTTP/Context/Features/IHttpStreamIdFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/IHttpStreamIdFeature.cs @@ -1,4 +1,4 @@ -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; internal interface IHttpStreamIdFeature { diff --git a/src/TurboHTTP/Context/Features/ITlsHandshakeFeature.cs b/src/TurboHTTP/Server/Context/Features/ITlsHandshakeFeature.cs similarity index 86% rename from src/TurboHTTP/Context/Features/ITlsHandshakeFeature.cs rename to src/TurboHTTP/Server/Context/Features/ITlsHandshakeFeature.cs index 1349e01aa..64c972c49 100644 --- a/src/TurboHTTP/Context/Features/ITlsHandshakeFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/ITlsHandshakeFeature.cs @@ -1,7 +1,7 @@ using System.Net.Security; using System.Security.Authentication; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; public interface ITlsHandshakeFeature { diff --git a/src/TurboHTTP/Context/Features/TlsHandshakeFeature.cs b/src/TurboHTTP/Server/Context/Features/TlsHandshakeFeature.cs similarity index 89% rename from src/TurboHTTP/Context/Features/TlsHandshakeFeature.cs rename to src/TurboHTTP/Server/Context/Features/TlsHandshakeFeature.cs index eeddd4072..bd9bafa73 100644 --- a/src/TurboHTTP/Context/Features/TlsHandshakeFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TlsHandshakeFeature.cs @@ -1,7 +1,7 @@ using System.Net.Security; using System.Security.Authentication; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; internal sealed class TlsHandshakeFeature : ITlsHandshakeFeature { diff --git a/src/TurboHTTP/Context/Features/TurboFeatureCollection.cs b/src/TurboHTTP/Server/Context/Features/TurboFeatureCollection.cs similarity index 51% rename from src/TurboHTTP/Context/Features/TurboFeatureCollection.cs rename to src/TurboHTTP/Server/Context/Features/TurboFeatureCollection.cs index 14f9d5a35..21e5cbae1 100644 --- a/src/TurboHTTP/Context/Features/TurboFeatureCollection.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboFeatureCollection.cs @@ -1,149 +1,163 @@ using System.Collections; +using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http.Features; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; -internal sealed class TurboFeatureCollection : ITurboFeatureCollection, IFeatureCollection +internal sealed class TurboFeatureCollection : IFeatureCollection { - private ITurboRequestFeature? _request; - private ITurboResponseFeature? _response; - private ITurboConnectionFeature? _connection; - private ITurboResponseBodyFeature? _responseBody; - private ITurboRequestBodyFeature? _requestBody; - private ITurboRequestBodyDetectionFeature? _bodyDetection; - private ITurboRequestLifetimeFeature? _lifetime; - private ITurboRequestIdentifierFeature? _identifier; - private ITurboResponseTrailersFeature? _trailers; - private ITurboResetFeature? _reset; + private IHttpRequestFeature? _request; + private IHttpResponseFeature? _response; + private IHttpConnectionFeature? _connection; + private IHttpResponseBodyFeature? _responseBody; + private IHttpRequestBodyDetectionFeature? _bodyDetection; + private IHttpRequestLifetimeFeature? _lifetime; + private IHttpRequestIdentifierFeature? _identifier; + private IHttpResponseTrailersFeature? _trailers; + private IHttpResetFeature? _reset; + private IHttpMaxRequestBodySizeFeature? _maxRequestBodySize; + private IHttpBodyControlFeature? _bodyControl; private Dictionary? _extras; + internal long RequestTimestamp { get; set; } + internal Activity? RequestActivity { get; set; } private int _revision; public T? Get() where T : class { - if (typeof(T) == typeof(ITurboRequestFeature) || typeof(T) == typeof(IHttpRequestFeature)) + if (typeof(T) == typeof(IHttpRequestFeature)) { return Unsafe.As(_request); } - if (typeof(T) == typeof(ITurboResponseFeature) || typeof(T) == typeof(IHttpResponseFeature)) + if (typeof(T) == typeof(IHttpResponseFeature)) { return Unsafe.As(_response); } - if (typeof(T) == typeof(ITurboConnectionFeature)) + if (typeof(T) == typeof(IHttpConnectionFeature)) { return Unsafe.As(_connection); } - if (typeof(T) == typeof(ITurboResponseBodyFeature) || typeof(T) == typeof(IHttpResponseBodyFeature)) + if (typeof(T) == typeof(IHttpResponseBodyFeature)) { return Unsafe.As(_responseBody); } - if (typeof(T) == typeof(ITurboRequestBodyFeature)) - { - return Unsafe.As(_requestBody); - } - - if (typeof(T) == typeof(ITurboRequestBodyDetectionFeature) || - typeof(T) == typeof(IHttpRequestBodyDetectionFeature)) + if (typeof(T) == typeof(IHttpRequestBodyDetectionFeature)) { return Unsafe.As(_bodyDetection); } - if (typeof(T) == typeof(ITurboRequestLifetimeFeature) || typeof(T) == typeof(IHttpRequestLifetimeFeature)) + if (typeof(T) == typeof(IHttpRequestLifetimeFeature)) { return Unsafe.As(_lifetime); } - if (typeof(T) == typeof(ITurboRequestIdentifierFeature) || typeof(T) == typeof(IHttpRequestIdentifierFeature)) + if (typeof(T) == typeof(IHttpRequestIdentifierFeature)) { return Unsafe.As(_identifier); } - if (typeof(T) == typeof(ITurboResponseTrailersFeature) || typeof(T) == typeof(IHttpResponseTrailersFeature)) + if (typeof(T) == typeof(IHttpResponseTrailersFeature)) { return Unsafe.As(_trailers); } - if (typeof(T) == typeof(ITurboResetFeature)) + if (typeof(T) == typeof(IHttpResetFeature)) { return Unsafe.As(_reset); } + if (typeof(T) == typeof(IHttpMaxRequestBodySizeFeature)) + { + return Unsafe.As(_maxRequestBodySize); + } + + if (typeof(T) == typeof(IHttpBodyControlFeature)) + { + return Unsafe.As(_bodyControl); + } + return _extras is not null && _extras.TryGetValue(typeof(T), out var val) ? (T)val : null; } public void Set(T? feature) where T : class { - if (typeof(T) == typeof(ITurboRequestFeature) || typeof(T) == typeof(IHttpRequestFeature)) + if (typeof(T) == typeof(IHttpRequestFeature)) { - _request = Unsafe.As(feature); + _request = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboResponseFeature) || typeof(T) == typeof(IHttpResponseFeature)) + if (typeof(T) == typeof(IHttpResponseFeature)) { - _response = Unsafe.As(feature); + _response = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboConnectionFeature)) + if (typeof(T) == typeof(IHttpConnectionFeature)) { - _connection = Unsafe.As(feature); + _connection = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboResponseBodyFeature) || typeof(T) == typeof(IHttpResponseBodyFeature)) + if (typeof(T) == typeof(IHttpResponseBodyFeature)) { - _responseBody = Unsafe.As(feature); + _responseBody = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboRequestBodyFeature)) + if (typeof(T) == typeof(IHttpRequestBodyDetectionFeature)) { - _requestBody = Unsafe.As(feature); + _bodyDetection = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboRequestBodyDetectionFeature) || - typeof(T) == typeof(IHttpRequestBodyDetectionFeature)) + if (typeof(T) == typeof(IHttpRequestLifetimeFeature)) { - _bodyDetection = Unsafe.As(feature); + _lifetime = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboRequestLifetimeFeature) || typeof(T) == typeof(IHttpRequestLifetimeFeature)) + if (typeof(T) == typeof(IHttpRequestIdentifierFeature)) { - _lifetime = Unsafe.As(feature); + _identifier = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboRequestIdentifierFeature) || typeof(T) == typeof(IHttpRequestIdentifierFeature)) + if (typeof(T) == typeof(IHttpResponseTrailersFeature)) { - _identifier = Unsafe.As(feature); + _trailers = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboResponseTrailersFeature) || typeof(T) == typeof(IHttpResponseTrailersFeature)) + if (typeof(T) == typeof(IHttpResetFeature)) { - _trailers = Unsafe.As(feature); + _reset = Unsafe.As(feature); _revision++; return; } - if (typeof(T) == typeof(ITurboResetFeature)) + if (typeof(T) == typeof(IHttpMaxRequestBodySizeFeature)) { - _reset = Unsafe.As(feature); + _maxRequestBodySize = Unsafe.As(feature); + _revision++; + return; + } + + if (typeof(T) == typeof(IHttpBodyControlFeature)) + { + _bodyControl = Unsafe.As(feature); _revision++; return; } @@ -190,7 +204,6 @@ public void Set(T? feature) where T : class return default; } - // Cast to object, then use reflection to call the class-constrained Get var result = GetCore(typeof(TFeature)); return (TFeature?)result; } @@ -207,89 +220,59 @@ public void Set(T? feature) where T : class private object? GetCore(Type type) { - if (type == typeof(ITurboRequestFeature)) - { - return _request; - } - if (type == typeof(IHttpRequestFeature)) { return _request; } - if (type == typeof(ITurboResponseFeature)) - { - return _response; - } - if (type == typeof(IHttpResponseFeature)) { return _response; } - if (type == typeof(ITurboConnectionFeature)) + if (type == typeof(IHttpConnectionFeature)) { return _connection; } - if (type == typeof(ITurboResponseBodyFeature)) - { - return _responseBody; - } - if (type == typeof(IHttpResponseBodyFeature)) { return _responseBody; } - if (type == typeof(ITurboRequestBodyFeature)) - { - return _requestBody; - } - - if (type == typeof(ITurboRequestBodyDetectionFeature)) - { - return _bodyDetection; - } - if (type == typeof(IHttpRequestBodyDetectionFeature)) { return _bodyDetection; } - if (type == typeof(ITurboRequestLifetimeFeature)) - { - return _lifetime; - } - if (type == typeof(IHttpRequestLifetimeFeature)) { return _lifetime; } - if (type == typeof(ITurboRequestIdentifierFeature)) + if (type == typeof(IHttpRequestIdentifierFeature)) { return _identifier; } - if (type == typeof(IHttpRequestIdentifierFeature)) + if (type == typeof(IHttpResponseTrailersFeature)) { - return _identifier; + return _trailers; } - if (type == typeof(ITurboResponseTrailersFeature)) + if (type == typeof(IHttpResetFeature)) { - return _trailers; + return _reset; } - if (type == typeof(IHttpResponseTrailersFeature)) + if (type == typeof(IHttpMaxRequestBodySizeFeature)) { - return _trailers; + return _maxRequestBodySize; } - if (type == typeof(ITurboResetFeature)) + if (type == typeof(IHttpBodyControlFeature)) { - return _reset; + return _bodyControl; } return _extras is not null && _extras.TryGetValue(type, out var val) ? val : null; @@ -297,72 +280,79 @@ public void Set(T? feature) where T : class private void SetCore(Type type, object? instance) { - if (type == typeof(ITurboRequestFeature) || type == typeof(IHttpRequestFeature)) + if (type == typeof(IHttpRequestFeature)) + { + _request = (IHttpRequestFeature?)instance; + _revision++; + return; + } + + if (type == typeof(IHttpResponseFeature)) { - _request = (ITurboRequestFeature?)instance; + _response = (IHttpResponseFeature?)instance; _revision++; return; } - if (type == typeof(ITurboResponseFeature) || type == typeof(IHttpResponseFeature)) + if (type == typeof(IHttpConnectionFeature)) { - _response = (ITurboResponseFeature?)instance; + _connection = (IHttpConnectionFeature?)instance; _revision++; return; } - if (type == typeof(ITurboConnectionFeature)) + if (type == typeof(IHttpResponseBodyFeature)) { - _connection = (ITurboConnectionFeature?)instance; + _responseBody = (IHttpResponseBodyFeature?)instance; _revision++; return; } - if (type == typeof(ITurboResponseBodyFeature) || type == typeof(IHttpResponseBodyFeature)) + if (type == typeof(IHttpRequestBodyDetectionFeature)) { - _responseBody = (ITurboResponseBodyFeature?)instance; + _bodyDetection = (IHttpRequestBodyDetectionFeature?)instance; _revision++; return; } - if (type == typeof(ITurboRequestBodyFeature)) + if (type == typeof(IHttpRequestLifetimeFeature)) { - _requestBody = (ITurboRequestBodyFeature?)instance; + _lifetime = (IHttpRequestLifetimeFeature?)instance; _revision++; return; } - if (type == typeof(ITurboRequestBodyDetectionFeature) || type == typeof(IHttpRequestBodyDetectionFeature)) + if (type == typeof(IHttpRequestIdentifierFeature)) { - _bodyDetection = (ITurboRequestBodyDetectionFeature?)instance; + _identifier = (IHttpRequestIdentifierFeature?)instance; _revision++; return; } - if (type == typeof(ITurboRequestLifetimeFeature) || type == typeof(IHttpRequestLifetimeFeature)) + if (type == typeof(IHttpResponseTrailersFeature)) { - _lifetime = (ITurboRequestLifetimeFeature?)instance; + _trailers = (IHttpResponseTrailersFeature?)instance; _revision++; return; } - if (type == typeof(ITurboRequestIdentifierFeature) || type == typeof(IHttpRequestIdentifierFeature)) + if (type == typeof(IHttpResetFeature)) { - _identifier = (ITurboRequestIdentifierFeature?)instance; + _reset = (IHttpResetFeature?)instance; _revision++; return; } - if (type == typeof(ITurboResponseTrailersFeature) || type == typeof(IHttpResponseTrailersFeature)) + if (type == typeof(IHttpMaxRequestBodySizeFeature)) { - _trailers = (ITurboResponseTrailersFeature?)instance; + _maxRequestBodySize = (IHttpMaxRequestBodySizeFeature?)instance; _revision++; return; } - if (type == typeof(ITurboResetFeature)) + if (type == typeof(IHttpBodyControlFeature)) { - _reset = (ITurboResetFeature?)instance; + _bodyControl = (IHttpBodyControlFeature?)instance; _revision++; return; } @@ -384,52 +374,57 @@ IEnumerator> IEnumerable>. { if (_request is not null) { - yield return new KeyValuePair(typeof(ITurboRequestFeature), _request); + yield return new KeyValuePair(typeof(IHttpRequestFeature), _request); } if (_response is not null) { - yield return new KeyValuePair(typeof(ITurboResponseFeature), _response); + yield return new KeyValuePair(typeof(IHttpResponseFeature), _response); } if (_connection is not null) { - yield return new KeyValuePair(typeof(ITurboConnectionFeature), _connection); + yield return new KeyValuePair(typeof(IHttpConnectionFeature), _connection); } if (_responseBody is not null) { - yield return new KeyValuePair(typeof(ITurboResponseBodyFeature), _responseBody); - } - - if (_requestBody is not null) - { - yield return new KeyValuePair(typeof(ITurboRequestBodyFeature), _requestBody); + yield return new KeyValuePair(typeof(IHttpResponseBodyFeature), _responseBody); } if (_bodyDetection is not null) { - yield return new KeyValuePair(typeof(ITurboRequestBodyDetectionFeature), _bodyDetection); + yield return new KeyValuePair(typeof(IHttpRequestBodyDetectionFeature), _bodyDetection); } if (_lifetime is not null) { - yield return new KeyValuePair(typeof(ITurboRequestLifetimeFeature), _lifetime); + yield return new KeyValuePair(typeof(IHttpRequestLifetimeFeature), _lifetime); } if (_identifier is not null) { - yield return new KeyValuePair(typeof(ITurboRequestIdentifierFeature), _identifier); + yield return new KeyValuePair(typeof(IHttpRequestIdentifierFeature), _identifier); } if (_trailers is not null) { - yield return new KeyValuePair(typeof(ITurboResponseTrailersFeature), _trailers); + yield return new KeyValuePair(typeof(IHttpResponseTrailersFeature), _trailers); } if (_reset is not null) { - yield return new KeyValuePair(typeof(ITurboResetFeature), _reset); + yield return new KeyValuePair(typeof(IHttpResetFeature), _reset); + } + + if (_maxRequestBodySize is not null) + { + yield return new KeyValuePair(typeof(IHttpMaxRequestBodySizeFeature), _maxRequestBodySize); + } + + if (_bodyControl is not null) + { + yield return new KeyValuePair(typeof(IHttpBodyControlFeature), _bodyControl); } if (_extras is not null) diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpBodyControlFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpBodyControlFeature.cs new file mode 100644 index 000000000..665ebaf06 --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpBodyControlFeature.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpBodyControlFeature : IHttpBodyControlFeature +{ + public bool AllowSynchronousIO { get; set; } +} diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpConnectionFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpConnectionFeature.cs new file mode 100644 index 000000000..c225fc44e --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpConnectionFeature.cs @@ -0,0 +1,17 @@ +using System.Net; +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpConnectionFeature : IHttpConnectionFeature +{ + public string ConnectionId { get; set; } = string.Empty; + + public IPAddress? RemoteIpAddress { get; set; } + + public int RemotePort { get; set; } + + public IPAddress? LocalIpAddress { get; set; } + + public int LocalPort { get; set; } +} \ No newline at end of file diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpMaxRequestBodySizeFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpMaxRequestBodySizeFeature.cs new file mode 100644 index 000000000..e4b766fa4 --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpMaxRequestBodySizeFeature.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature +{ + public bool IsReadOnly { get; set; } + public long? MaxRequestBodySize { get; set; } +} diff --git a/src/TurboHTTP/Context/Features/TurboHttpRequestBodyDetectionFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestBodyDetectionFeature.cs similarity index 54% rename from src/TurboHTTP/Context/Features/TurboHttpRequestBodyDetectionFeature.cs rename to src/TurboHTTP/Server/Context/Features/TurboHttpRequestBodyDetectionFeature.cs index b970487b8..a33871db5 100644 --- a/src/TurboHTTP/Context/Features/TurboHttpRequestBodyDetectionFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestBodyDetectionFeature.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; internal sealed class TurboHttpRequestBodyDetectionFeature(bool canHaveBody) - : IHttpRequestBodyDetectionFeature, ITurboRequestBodyDetectionFeature + : IHttpRequestBodyDetectionFeature { public bool CanHaveBody { get; } = canHaveBody; } \ No newline at end of file diff --git a/src/TurboHTTP/Context/Features/TurboHttpRequestFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestFeature.cs similarity index 65% rename from src/TurboHTTP/Context/Features/TurboHttpRequestFeature.cs rename to src/TurboHTTP/Server/Context/Features/TurboHttpRequestFeature.cs index 0433d9a88..0e56bcebd 100644 --- a/src/TurboHTTP/Context/Features/TurboHttpRequestFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestFeature.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Adapters; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; -internal sealed class TurboHttpRequestFeature : IHttpRequestFeature, ITurboRequestFeature +internal sealed class TurboHttpRequestFeature : IHttpRequestFeature { private readonly TurboResponseHeaderDictionary _headers = new(); @@ -42,22 +40,4 @@ public IHeaderDictionary Headers } internal string? ExtractedHost { get; set; } - - IHeaderDictionary IHttpRequestFeature.Headers - { - get => _headers; - set - { - if (value is not null) - { - _headers.Clear(); - foreach (var kvp in value) - { - _headers[kvp.Key] = kvp.Value; - } - } - } - } - - ITurboHeaderDictionary ITurboRequestFeature.Headers => _headers; } diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpRequestIdentifierFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestIdentifierFeature.cs new file mode 100644 index 000000000..cefb079fd --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestIdentifierFeature.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature +{ + public string TraceIdentifier + { + get => field ??= Guid.NewGuid().ToString("N"); + set; + } +} diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpRequestLifetimeFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestLifetimeFeature.cs new file mode 100644 index 000000000..b4137930b --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpRequestLifetimeFeature.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http.Features; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature +{ + public CancellationToken RequestAborted { get; set; } + + public void Abort() => RequestAborted = new CancellationToken(true); +} diff --git a/src/TurboHTTP/Context/Features/TurboHttpResetFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpResetFeature.cs similarity index 64% rename from src/TurboHTTP/Context/Features/TurboHttpResetFeature.cs rename to src/TurboHTTP/Server/Context/Features/TurboHttpResetFeature.cs index f7553b360..19bfa85d3 100644 --- a/src/TurboHTTP/Context/Features/TurboHttpResetFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpResetFeature.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; -internal sealed class TurboHttpResetFeature : IHttpResetFeature, ITurboResetFeature +internal sealed class TurboHttpResetFeature : IHttpResetFeature { private readonly Action _resetCallback; diff --git a/src/TurboHTTP/Server/Context/Features/TurboHttpResponseBodyFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseBodyFeature.cs new file mode 100644 index 000000000..137affdf9 --- /dev/null +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseBodyFeature.cs @@ -0,0 +1,234 @@ +using System.Buffers; +using System.IO.Pipelines; +using Akka; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; +using Servus.Akka.Streams.IO; + +namespace TurboHTTP.Server.Context.Features; + +internal sealed class TurboHttpResponseBodyFeature : IHttpResponseBodyFeature +{ + private readonly Pipe _pipe = new(); + private readonly ResponsePipeWriter _writer; + + public TurboHttpResponseBodyFeature() + { + _writer = new ResponsePipeWriter(_pipe.Writer); + } + + internal void SetOnStarting(Func onStarting) => _writer.SetOnStarting(onStarting); + + internal bool HasStarted => _writer.HasStarted; + + internal Task WhenHeadersReady => _writer.WhenHeadersReady; + + public Stream Stream => field ??= _writer.AsStream(leaveOpen: true); + + public PipeWriter Writer => _writer; + + public Task WhenSinkCompleted => Task.CompletedTask; + + public Sink, Task> BodySink + { + get + { + if (field == null) + { + var pipeSink = PipeSink.To(_pipe.Writer); + field = Flow.Create>() + .SelectAsync(1, chunk => + { + _writer.CommitHeaders(); + return Task.FromResult(chunk); + }) + .ToMaterialized(pipeSink, Keep.Right); + } + + return field; + } + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + _writer.CommitHeaders(); + return Task.CompletedTask; + } + + public async Task SendFileAsync(string path, long offset, long? count, + CancellationToken cancellationToken = default) + { + await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4 * 1024, + useAsync: true); + if (offset > 0) + { + fs.Seek(offset, SeekOrigin.Begin); + } + + var remaining = count ?? long.MaxValue; + var buffer = ArrayPool.Shared.Rent(4 * 1024); + try + { + while (remaining > 0) + { + var toRead = (int)Math.Min(buffer.Length, remaining); + var read = await fs.ReadAsync(buffer.AsMemory(0, toRead), cancellationToken); + if (read == 0) + { + break; + } + + var dest = _writer.GetMemory(read); + buffer.AsSpan(0, read).CopyTo(dest.Span); + _writer.Advance(read); + await _writer.FlushAsync(cancellationToken); + remaining -= read; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + internal void Complete() + { + _writer.Complete(); + } + + public Task CompleteAsync() + { + return _writer.CompleteAsync().AsTask(); + } + + public void DisableBuffering() + { + } + + internal Source, NotUsed> GetResponseSource() + { + return PipeSource.From(_pipe.Reader); + } + + internal Stream GetResponseStream() => _pipe.Reader.AsStream(); + + internal sealed class ResponsePipeWriter : PipeWriter + { + private readonly PipeWriter _inner; + private readonly TaskCompletionSource _headerCommit = new(TaskCreationOptions.RunContinuationsAsynchronously); + private Func? _onStarting; + private bool _started; + private bool _completed; + private long _bytesWritten; + + public ResponsePipeWriter(PipeWriter inner) + { + _inner = inner; + } + + public Task WhenHeadersReady => _headerCommit.Task; + public bool HasStarted => _started; + public long BytesWritten => _bytesWritten; + + public void SetOnStarting(Func onStarting) => _onStarting = onStarting; + + public void CommitHeaders() + { + if (!_started) + { + _started = true; + _headerCommit.TrySetResult(); + } + } + + public override bool CanGetUnflushedBytes => _inner.CanGetUnflushedBytes; + public override long UnflushedBytes => _inner.UnflushedBytes; + public override Memory GetMemory(int sizeHint = 0) => _inner.GetMemory(sizeHint); + public override Span GetSpan(int sizeHint = 0) => _inner.GetSpan(sizeHint); + + public override void Advance(int bytes) + { + _inner.Advance(bytes); + _bytesWritten += bytes; + } + + public override void CancelPendingFlush() => _inner.CancelPendingFlush(); + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_started) + { + return _inner.FlushAsync(cancellationToken); + } + + return CommitAndFlushAsync(cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + if (_started) + { + return _inner.WriteAsync(source, cancellationToken); + } + + return CommitAndWriteAsync(source, cancellationToken); + } + + private async ValueTask CommitAndFlushAsync(CancellationToken cancellationToken) + { + _started = true; + try + { + if (_onStarting is not null) + { + await _onStarting(); + } + } + finally + { + _headerCommit.TrySetResult(); + } + + return await _inner.FlushAsync(cancellationToken); + } + + private async ValueTask CommitAndWriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken) + { + _started = true; + try + { + if (_onStarting is not null) + { + await _onStarting(); + } + } + finally + { + _headerCommit.TrySetResult(); + } + + _bytesWritten += source.Length; + return await _inner.WriteAsync(source, cancellationToken); + } + + public override void Complete(Exception? exception = null) + { + if (!_completed) + { + _completed = true; + _inner.Complete(exception); + } + } + + public override ValueTask CompleteAsync(Exception? exception = null) + { + if (!_completed) + { + _completed = true; + return _inner.CompleteAsync(exception); + } + + return default; + } + } +} diff --git a/src/TurboHTTP/Context/Features/TurboHttpResponseFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseFeature.cs similarity index 74% rename from src/TurboHTTP/Context/Features/TurboHttpResponseFeature.cs rename to src/TurboHTTP/Server/Context/Features/TurboHttpResponseFeature.cs index 4ca1eea18..473615e95 100644 --- a/src/TurboHTTP/Context/Features/TurboHttpResponseFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseFeature.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Adapters; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; -internal sealed class TurboHttpResponseFeature : IHttpResponseFeature, ITurboResponseFeature +internal sealed class TurboHttpResponseFeature : IHttpResponseFeature { private readonly TurboResponseHeaderDictionary _headers = new(); private readonly List<(Func callback, object? state)> _onStartingCallbacks = []; @@ -19,7 +17,11 @@ internal sealed class TurboHttpResponseFeature : IHttpResponseFeature, ITurboRes public bool HasStarted { get; private set; } - public IHeaderDictionary Headers => _headers; + public IHeaderDictionary Headers + { + get => _headers; + set { } + } public void OnStarting(Func callback, object? state) { @@ -36,33 +38,15 @@ public void OnCompleted(Func callback, object? state) void IHttpResponseFeature.OnStarting(Func callback, object state) { ArgumentNullException.ThrowIfNull(callback); - OnStarting((Func)callback!, state!); + OnStarting((Func)callback, state); } void IHttpResponseFeature.OnCompleted(Func callback, object state) { ArgumentNullException.ThrowIfNull(callback); - OnCompleted((Func)callback!, state!); - } - - void ITurboResponseFeature.OnStarting(Func callback, object? state) - { - OnStarting(callback, state); - } - - void ITurboResponseFeature.OnCompleted(Func callback, object? state) - { - OnCompleted(callback, state); - } - - IHeaderDictionary IHttpResponseFeature.Headers - { - get => _headers; - set { } + OnCompleted((Func)callback, state); } - ITurboHeaderDictionary ITurboResponseFeature.Headers => _headers; - internal async Task FireOnStartingAsync() { HasStarted = true; diff --git a/src/TurboHTTP/Context/Features/TurboHttpResponseTrailersFeature.cs b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseTrailersFeature.cs similarity index 61% rename from src/TurboHTTP/Context/Features/TurboHttpResponseTrailersFeature.cs rename to src/TurboHTTP/Server/Context/Features/TurboHttpResponseTrailersFeature.cs index 448860113..0e95dffbe 100644 --- a/src/TurboHTTP/Context/Features/TurboHttpResponseTrailersFeature.cs +++ b/src/TurboHTTP/Server/Context/Features/TurboHttpResponseTrailersFeature.cs @@ -1,29 +1,19 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Context.Adapters; using TurboHTTP.Protocol.Semantics; -namespace TurboHTTP.Context.Features; +namespace TurboHTTP.Server.Context.Features; -internal sealed class TurboHttpResponseTrailersFeature : IHttpResponseTrailersFeature, ITurboResponseTrailersFeature +internal sealed class TurboHttpResponseTrailersFeature : IHttpResponseTrailersFeature { private TurboResponseHeaderDictionary _trailers = new(); - public IHeaderDictionary Trailers => _trailers; - - IHeaderDictionary IHttpResponseTrailersFeature.Trailers + public IHeaderDictionary Trailers { get => _trailers; set { } } - ITurboHeaderDictionary ITurboResponseTrailersFeature.Trailers - { - get => _trailers; - set => _trailers = (TurboResponseHeaderDictionary)value; - } - public IEnumerable> GetAllowedTrailers() { foreach (var header in _trailers) diff --git a/src/TurboHTTP/Context/ITurboFormCollection.cs b/src/TurboHTTP/Server/Context/ITurboFormCollection.cs similarity index 94% rename from src/TurboHTTP/Context/ITurboFormCollection.cs rename to src/TurboHTTP/Server/Context/ITurboFormCollection.cs index a570db084..db5ae1286 100644 --- a/src/TurboHTTP/Context/ITurboFormCollection.cs +++ b/src/TurboHTTP/Server/Context/ITurboFormCollection.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Primitives; -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; public interface ITurboFormCollection : IEnumerable> { diff --git a/src/TurboHTTP/Context/ITurboFormFile.cs b/src/TurboHTTP/Server/Context/ITurboFormFile.cs similarity index 89% rename from src/TurboHTTP/Context/ITurboFormFile.cs rename to src/TurboHTTP/Server/Context/ITurboFormFile.cs index 849d4302f..d4948002c 100644 --- a/src/TurboHTTP/Context/ITurboFormFile.cs +++ b/src/TurboHTTP/Server/Context/ITurboFormFile.cs @@ -1,4 +1,4 @@ -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; public interface ITurboFormFile { diff --git a/src/TurboHTTP/Context/ITurboHeaderDictionary.cs b/src/TurboHTTP/Server/Context/ITurboHeaderDictionary.cs similarity index 92% rename from src/TurboHTTP/Context/ITurboHeaderDictionary.cs rename to src/TurboHTTP/Server/Context/ITurboHeaderDictionary.cs index 39fca5ffd..a0bdb9632 100644 --- a/src/TurboHTTP/Context/ITurboHeaderDictionary.cs +++ b/src/TurboHTTP/Server/Context/ITurboHeaderDictionary.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Primitives; -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; public interface ITurboHeaderDictionary : IEnumerable> { diff --git a/src/TurboHTTP/Context/ITurboQueryCollection.cs b/src/TurboHTTP/Server/Context/ITurboQueryCollection.cs similarity index 90% rename from src/TurboHTTP/Context/ITurboQueryCollection.cs rename to src/TurboHTTP/Server/Context/ITurboQueryCollection.cs index b5c0725da..594e845f0 100644 --- a/src/TurboHTTP/Context/ITurboQueryCollection.cs +++ b/src/TurboHTTP/Server/Context/ITurboQueryCollection.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Primitives; -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; public interface ITurboQueryCollection : IEnumerable> { diff --git a/src/TurboHTTP/Context/ITurboRequestCookieCollection.cs b/src/TurboHTTP/Server/Context/ITurboRequestCookieCollection.cs similarity index 86% rename from src/TurboHTTP/Context/ITurboRequestCookieCollection.cs rename to src/TurboHTTP/Server/Context/ITurboRequestCookieCollection.cs index 116db13a0..92e682ac8 100644 --- a/src/TurboHTTP/Context/ITurboRequestCookieCollection.cs +++ b/src/TurboHTTP/Server/Context/ITurboRequestCookieCollection.cs @@ -1,4 +1,4 @@ -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; public interface ITurboRequestCookieCollection : IEnumerable> { diff --git a/src/TurboHTTP/Context/TurboFormCollection.cs b/src/TurboHTTP/Server/Context/TurboFormCollection.cs similarity index 98% rename from src/TurboHTTP/Context/TurboFormCollection.cs rename to src/TurboHTTP/Server/Context/TurboFormCollection.cs index 47f2dadfc..9e36de12c 100644 --- a/src/TurboHTTP/Context/TurboFormCollection.cs +++ b/src/TurboHTTP/Server/Context/TurboFormCollection.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; internal sealed class TurboFormCollection(Dictionary fields, IFormFileCollection files) : IFormCollection, ITurboFormCollection { diff --git a/src/TurboHTTP/Context/TurboFormFile.cs b/src/TurboHTTP/Server/Context/TurboFormFile.cs similarity index 96% rename from src/TurboHTTP/Context/TurboFormFile.cs rename to src/TurboHTTP/Server/Context/TurboFormFile.cs index 6bcc3215e..1466a886b 100644 --- a/src/TurboHTTP/Context/TurboFormFile.cs +++ b/src/TurboHTTP/Server/Context/TurboFormFile.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; -namespace TurboHTTP.Context; +namespace TurboHTTP.Server.Context; internal sealed class TurboFormFile : IFormFile, ITurboFormFile { diff --git a/src/TurboHTTP/Context/Adapters/TurboResponseHeaderDictionary.cs b/src/TurboHTTP/Server/Context/TurboResponseHeaderDictionary.cs similarity index 98% rename from src/TurboHTTP/Context/Adapters/TurboResponseHeaderDictionary.cs rename to src/TurboHTTP/Server/Context/TurboResponseHeaderDictionary.cs index 5fcc0e451..fc9779149 100644 --- a/src/TurboHTTP/Context/Adapters/TurboResponseHeaderDictionary.cs +++ b/src/TurboHTTP/Server/Context/TurboResponseHeaderDictionary.cs @@ -4,7 +4,7 @@ using TurboHTTP.Protocol; using TurboHTTP.Protocol.Semantics; -namespace TurboHTTP.Context.Adapters; +namespace TurboHTTP.Server.Context; internal sealed class TurboResponseHeaderDictionary : IHeaderDictionary, ITurboHeaderDictionary { diff --git a/src/TurboHTTP/Routing/EndpointResolver.cs b/src/TurboHTTP/Server/EndpointResolver.cs similarity index 99% rename from src/TurboHTTP/Routing/EndpointResolver.cs rename to src/TurboHTTP/Server/EndpointResolver.cs index 1ef36234d..f952df740 100644 --- a/src/TurboHTTP/Routing/EndpointResolver.cs +++ b/src/TurboHTTP/Server/EndpointResolver.cs @@ -5,9 +5,8 @@ using Servus.Akka.Transport; using Servus.Akka.Transport.Quic.Listener; using Servus.Akka.Transport.Tcp.Listener; -using TurboHTTP.Server; -namespace TurboHTTP.Routing; +namespace TurboHTTP.Server; internal sealed class EndpointResolver { diff --git a/src/TurboHTTP/Server/FeatureCollectionFactory.cs b/src/TurboHTTP/Server/FeatureCollectionFactory.cs new file mode 100644 index 000000000..039f89509 --- /dev/null +++ b/src/TurboHTTP/Server/FeatureCollectionFactory.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Http.Features; +using TurboHTTP.Server.Context.Features; + +namespace TurboHTTP.Server; + +internal static class FeatureCollectionFactory +{ + [ThreadStatic] private static Stack? _tPool; + + private const int MaxPoolSize = 32; + + public static IFeatureCollection Create( + TurboHttpRequestFeature requestFeature, + bool hasBody, + IServiceProvider? services = null, + IHttpConnectionFeature? connectionFeature = null, + TlsHandshakeFeature? tlsFeature = null, + long? maxRequestBodySize = null) + { + var features = (_tPool?.Count ?? 0) > 0 ? _tPool!.Pop() : new TurboFeatureCollection(); + + features.Set(requestFeature); + + var responseFeature = new TurboHttpResponseFeature(); + features.Set(responseFeature); + + var detectionFeature = new TurboHttpRequestBodyDetectionFeature(hasBody); + features.Set(detectionFeature); + + var responseBodyFeature = new TurboHttpResponseBodyFeature(); + features.Set(responseBodyFeature); + + var trailersFeature = new TurboHttpResponseTrailersFeature(); + features.Set(trailersFeature); + + if (connectionFeature is not null) + { + features.Set(connectionFeature); + } + + if (tlsFeature is not null) + { + features.Set(tlsFeature); + } + + var lifetimeFeature = new TurboHttpRequestLifetimeFeature(); + features.Set(lifetimeFeature); + + var identifierFeature = new TurboHttpRequestIdentifierFeature(); + features.Set(identifierFeature); + + var maxBodyFeature = new TurboHttpMaxRequestBodySizeFeature + { + MaxRequestBodySize = maxRequestBodySize + }; + features.Set(maxBodyFeature); + + var bodyControlFeature = new TurboHttpBodyControlFeature(); + features.Set(bodyControlFeature); + + return features; + } + + internal static void Return(IFeatureCollection features) + { + if (features is not TurboFeatureCollection turboFeatures) + { + return; + } + + turboFeatures.RequestTimestamp = 0; + turboFeatures.RequestActivity = null; + + _tPool ??= new Stack(MaxPoolSize); + + if (_tPool.Count < MaxPoolSize) + { + _tPool.Push(turboFeatures); + } + } +} \ No newline at end of file diff --git a/src/TurboHTTP/Server/Hosting/TurboKestrelConfigurationBinder.cs b/src/TurboHTTP/Server/Hosting/TurboConfigurationBinder.cs similarity index 98% rename from src/TurboHTTP/Server/Hosting/TurboKestrelConfigurationBinder.cs rename to src/TurboHTTP/Server/Hosting/TurboConfigurationBinder.cs index 5d87c80a7..500f38524 100644 --- a/src/TurboHTTP/Server/Hosting/TurboKestrelConfigurationBinder.cs +++ b/src/TurboHTTP/Server/Hosting/TurboConfigurationBinder.cs @@ -4,7 +4,7 @@ namespace TurboHTTP.Server.Hosting; -internal static class TurboKestrelConfigurationBinder +internal static class TurboConfigurationBinder { public static void Bind(TurboServerOptions options, IConfigurationSection section) { diff --git a/src/TurboHTTP/Server/Http2ServerOptions.cs b/src/TurboHTTP/Server/Http2ServerOptions.cs index 91cffd0a9..4501a6ff8 100644 --- a/src/TurboHTTP/Server/Http2ServerOptions.cs +++ b/src/TurboHTTP/Server/Http2ServerOptions.cs @@ -6,8 +6,8 @@ public sealed class Http2ServerOptions public int InitialConnectionWindowSize { get; set; } = 1 * 1024 * 1024; public int InitialStreamWindowSize { get; set; } = 768 * 1024; public int MaxFrameSize { get; set; } = 16 * 1024; - public int MaxHeaderListSize { get; set; } = 32 * 1024; public int HeaderTableSize { get; set; } = 4 * 1024; + public int MaxHeaderListSize { get; set; } = 32 * 1024; public long MaxRequestBodySize { get; set; } = 30_000_000; public long MaxResponseBufferSize { get; set; } = 64 * 1024; public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(130); diff --git a/src/TurboHTTP/Server/IRouteTableAccessor.cs b/src/TurboHTTP/Server/IRouteTableAccessor.cs deleted file mode 100644 index 9f4768d6d..000000000 --- a/src/TurboHTTP/Server/IRouteTableAccessor.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -internal interface IRouteTableAccessor -{ - TurboRouteTable RouteTable { get; } -} diff --git a/src/TurboHTTP/Server/ITurboApplicationBuilder.cs b/src/TurboHTTP/Server/ITurboApplicationBuilder.cs deleted file mode 100644 index e81c2a576..000000000 --- a/src/TurboHTTP/Server/ITurboApplicationBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace TurboHTTP.Server; - -public interface ITurboApplicationBuilder -{ - ITurboApplicationBuilder Use(Func middleware); - - ITurboApplicationBuilder Use() where T : class, ITurboMiddleware; - - ITurboApplicationBuilder Run(TurboRequestDelegate handler); - - ITurboApplicationBuilder Map(string pathPrefix, Action configure); - - ITurboApplicationBuilder MapWhen(Func predicate, Action configure); -} diff --git a/src/TurboHTTP/Server/ITurboEndpointRouteBuilder.cs b/src/TurboHTTP/Server/ITurboEndpointRouteBuilder.cs deleted file mode 100644 index 1003b2af6..000000000 --- a/src/TurboHTTP/Server/ITurboEndpointRouteBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public interface ITurboEndpointRouteBuilder -{ - IServiceProvider ServiceProvider { get; } - - [Obsolete("Use extension methods to register routes. Direct RouteTable access will be removed in 2.0.")] - TurboRouteTable RouteTable { get; } -} diff --git a/src/TurboHTTP/Server/ITurboMiddleware.cs b/src/TurboHTTP/Server/ITurboMiddleware.cs deleted file mode 100644 index 07cdc3e11..000000000 --- a/src/TurboHTTP/Server/ITurboMiddleware.cs +++ /dev/null @@ -1,8 +0,0 @@ -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Server; - -public interface ITurboMiddleware -{ - Task InvokeAsync(TurboHttpContext context, TurboRequestDelegate next); -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/Middleware/TurboPipelineBuilder.cs b/src/TurboHTTP/Server/Middleware/TurboPipelineBuilder.cs deleted file mode 100644 index 76772702b..000000000 --- a/src/TurboHTTP/Server/Middleware/TurboPipelineBuilder.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace TurboHTTP.Server.Middleware; - -public sealed class TurboPipelineBuilder : ITurboApplicationBuilder -{ - private readonly List> _components = []; - - public ITurboApplicationBuilder Use(Func middleware) - { - _components.Add(next => ctx => middleware(ctx, next)); - return this; - } - - public ITurboApplicationBuilder Use() where T : class, ITurboMiddleware - { - _components.Add(next => ctx => - { - var mw = ctx.RequestServices.GetRequiredService(); - return mw.InvokeAsync(ctx, next); - }); - return this; - } - - public ITurboApplicationBuilder Run(TurboRequestDelegate handler) - { - _components.Add(_ => handler); - return this; - } - - public ITurboApplicationBuilder Map(string pathPrefix, Action configure) - { - var branch = new TurboPipelineBuilder(); - configure(branch); - - _components.Add(next => - { - TurboRequestDelegate? builtBranch = null; - return ctx => - { - var path = ctx.Request.Path.Value ?? string.Empty; - if (path.StartsWith(pathPrefix, StringComparison.OrdinalIgnoreCase)) - { - builtBranch ??= branch.BuildDelegate(next); - return builtBranch(ctx); - } - - return next(ctx); - }; - }); - return this; - } - - public ITurboApplicationBuilder MapWhen(Func predicate, - Action configure) - { - var branch = new TurboPipelineBuilder(); - configure(branch); - - _components.Add(next => - { - TurboRequestDelegate? builtBranch = null; - return ctx => - { - if (predicate(ctx)) - { - builtBranch ??= branch.BuildDelegate(next); - return builtBranch(ctx); - } - - return next(ctx); - }; - }); - return this; - } - - internal TurboRequestDelegate Build() - { - return BuildDelegate(Terminal); - Task Terminal(TurboHttpContext _) => Task.CompletedTask; - } - - private TurboRequestDelegate BuildDelegate(TurboRequestDelegate terminal) - { - var pipeline = terminal; - for (var i = _components.Count - 1; i >= 0; i--) - { - pipeline = _components[i](pipeline); - } - - return pipeline; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/ServerContextFactory.cs b/src/TurboHTTP/Server/ServerContextFactory.cs deleted file mode 100644 index 8f4a50a4f..000000000 --- a/src/TurboHTTP/Server/ServerContextFactory.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context.Features; - -namespace TurboHTTP.Server; - -internal static class ServerContextFactory -{ - [ThreadStatic] - private static Stack? t_pool; - - private const int MaxPoolSize = 32; - - public static TurboHttpContext Create( - TurboHttpRequestFeature requestFeature, - bool hasBody, - IServiceProvider? services = null, - TurboConnectionInfo? connectionInfo = null, - TlsHandshakeFeature? tlsFeature = null) - { - var features = new TurboFeatureCollection(); - - features.Set(requestFeature); - features.Set(requestFeature); - - var bodyFeature = new TurboRequestBodyFeature { Body = requestFeature.Body }; - features.Set(bodyFeature); - - var responseFeature = new TurboHttpResponseFeature(); - features.Set(responseFeature); - features.Set(responseFeature); - - var detectionFeature = new TurboHttpRequestBodyDetectionFeature(hasBody); - features.Set(detectionFeature); - features.Set(detectionFeature); - - var responseBodyFeature = new TurboHttpResponseBodyFeature(); - features.Set(responseBodyFeature); - features.Set(responseBodyFeature); - - var trailersFeature = new TurboHttpResponseTrailersFeature(); - features.Set(trailersFeature); - features.Set(trailersFeature); - - if (tlsFeature is not null) - { - features.Set(tlsFeature); - } - - TurboHttpContext ctx; - var pooledConnection = connectionInfo is not null; - var pooledServices = services is not null; - - if ((t_pool?.Count ?? 0) > 0 && pooledConnection && pooledServices) - { - ctx = t_pool!.Pop(); - ctx.Reset(features, connectionInfo!, services, CancellationToken.None, null!); - } - else if (pooledConnection) - { - ctx = new TurboHttpContext(features, connectionInfo, services, CancellationToken.None, null!); - } - else - { - ctx = new TurboHttpContext(features); - if (services is not null) - { - ctx.RequestServices = services; - } - } - - var lifetimeFeature = new TurboHttpRequestLifetimeFeature(ctx); - features.Set(lifetimeFeature); - features.Set(lifetimeFeature); - - var identifierFeature = new TurboHttpRequestIdentifierFeature(ctx); - features.Set(identifierFeature); - features.Set(identifierFeature); - - return ctx; - } - - internal static void Return(TurboHttpContext context) - { - t_pool ??= new Stack(MaxPoolSize); - - if (t_pool.Count < MaxPoolSize) - { - t_pool.Push(context); - } - } -} diff --git a/src/TurboHTTP/Server/TurboConnectionInfo.cs b/src/TurboHTTP/Server/TurboConnectionInfo.cs deleted file mode 100644 index 2d41f810f..000000000 --- a/src/TurboHTTP/Server/TurboConnectionInfo.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Http; - -namespace TurboHTTP.Server; - -public sealed class TurboConnectionInfo : ConnectionInfo -{ - private SslStream? _sslStream; - private bool _allowDelayedNegotiation; - private SslApplicationProtocol _negotiatedProtocol; - - public override string Id { get; set; } - public override IPAddress? RemoteIpAddress { get; set; } - public override int RemotePort { get; set; } - public override IPAddress? LocalIpAddress { get; set; } - public override int LocalPort { get; set; } - public override X509Certificate2? ClientCertificate { get; set; } - - public TurboConnectionInfo( - string id, - IPAddress? remoteIpAddress, - int remotePort, - IPAddress? localIpAddress, - int localPort) - { - Id = id; - RemoteIpAddress = remoteIpAddress; - RemotePort = remotePort; - LocalIpAddress = localIpAddress; - LocalPort = localPort; - } - - internal void SetTlsState(SslStream? sslStream, bool allowDelayedNegotiation) - { - _sslStream = sslStream; - _allowDelayedNegotiation = allowDelayedNegotiation; - } - - internal void SetNegotiatedProtocol(SslApplicationProtocol protocol) - { - _negotiatedProtocol = protocol; - } - - internal Servus.Akka.Transport.SecurityInfo? SecurityInfo { get; private set; } - - internal void SetSecurityInfo(Servus.Akka.Transport.SecurityInfo securityInfo) - { - SecurityInfo = securityInfo; - } - - internal void SetClientCertificateFromHandshake(SslStream sslStream) - { - if (sslStream.RemoteCertificate is X509Certificate2 cert) - { - ClientCertificate = cert; - } - } - - public override async Task GetClientCertificateAsync( - CancellationToken cancellationToken = default) - { - if (ClientCertificate is not null) - { - return ClientCertificate; - } - - if (_sslStream is null || !_allowDelayedNegotiation) - { - return null; - } - - if (_negotiatedProtocol != SslApplicationProtocol.Http11 && - _negotiatedProtocol != default) - { - throw new InvalidOperationException( - "Delayed client certificate negotiation is only supported on HTTP/1.1 connections."); - } - - await _sslStream.NegotiateClientCertificateAsync(cancellationToken); - ClientCertificate = _sslStream.RemoteCertificate as X509Certificate2; - return ClientCertificate; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboEndpointRouteBuilderExtensions.cs b/src/TurboHTTP/Server/TurboEndpointRouteBuilderExtensions.cs deleted file mode 100644 index d642ec913..000000000 --- a/src/TurboHTTP/Server/TurboEndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public static class TurboEndpointRouteBuilderExtensions -{ - private static TurboRouteTable GetTable(ITurboEndpointRouteBuilder builder) - { - if (builder is IRouteTableAccessor accessor) - { - return accessor.RouteTable; - } - -#pragma warning disable CS0618 - return builder.RouteTable; -#pragma warning restore CS0618 - } - - public static TurboRouteHandlerBuilder MapGet(this ITurboEndpointRouteBuilder builder, string pattern, Delegate handler) - { - return GetTable(builder).Add("GET", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapGet(this ITurboEndpointRouteBuilder builder, string pattern, Func handler) - { - return GetTable(builder).Add("GET", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPost(this ITurboEndpointRouteBuilder builder, string pattern, Delegate handler) - { - return GetTable(builder).Add("POST", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPost(this ITurboEndpointRouteBuilder builder, string pattern, Func handler) - { - return GetTable(builder).Add("POST", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPut(this ITurboEndpointRouteBuilder builder, string pattern, Delegate handler) - { - return GetTable(builder).Add("PUT", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPut(this ITurboEndpointRouteBuilder builder, string pattern, Func handler) - { - return GetTable(builder).Add("PUT", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapDelete(this ITurboEndpointRouteBuilder builder, string pattern, Delegate handler) - { - return GetTable(builder).Add("DELETE", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapDelete(this ITurboEndpointRouteBuilder builder, string pattern, Func handler) - { - return GetTable(builder).Add("DELETE", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPatch(this ITurboEndpointRouteBuilder builder, string pattern, Delegate handler) - { - return GetTable(builder).Add("PATCH", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapPatch(this ITurboEndpointRouteBuilder builder, string pattern, Func handler) - { - return GetTable(builder).Add("PATCH", pattern, handler); - } - - public static TurboRouteHandlerBuilder MapMethods(this ITurboEndpointRouteBuilder builder, string pattern, IEnumerable methods, Delegate handler) - { - TurboRouteHandlerBuilder? last = null; - foreach (var method in methods) - { - last = GetTable(builder).Add(method, pattern, handler); - } - - return last!; - } - - public static TurboRouteHandlerBuilder MapMethods(this ITurboEndpointRouteBuilder builder, string pattern, IEnumerable methods, Func handler) - { - TurboRouteHandlerBuilder? last = null; - foreach (var method in methods) - { - last = GetTable(builder).Add(method, pattern, handler); - } - - return last!; - } - - public static TurboRouteGroupBuilder MapGroup(this ITurboEndpointRouteBuilder builder, string prefix) - { - return GetTable(builder).CreateGroup(prefix); - } - - public static TurboRouteHandlerBuilder MapEntity(this ITurboEndpointRouteBuilder builder, string pattern, Action configure) - { - var entityBuilder = new TurboEntityBuilder(pattern); - configure(entityBuilder); - entityBuilder.AddToRouteTable(GetTable(builder)); - return new TurboRouteHandlerBuilder(); - } - - public static TurboRouteHandlerBuilder MapEntity(this ITurboEndpointRouteBuilder builder, string pattern, Action configure) - { - var entityBuilder = new TurboEntityBuilder(pattern).UseActorRef(x => x.Get()); - configure(entityBuilder); - entityBuilder.AddToRouteTable(GetTable(builder)); - return new TurboRouteHandlerBuilder(); - } -} diff --git a/src/TurboHTTP/Server/TurboEntityAskBuilder.cs b/src/TurboHTTP/Server/TurboEntityAskBuilder.cs deleted file mode 100644 index a6ccb1814..000000000 --- a/src/TurboHTTP/Server/TurboEntityAskBuilder.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.AspNetCore.Http; -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public sealed class TurboEntityAskBuilder -{ - internal EntityResponseMapperCollection Mappers { get; } = new(); - internal TimeSpan? TimeoutOverride { get; private set; } - - public TurboEntityAskBuilder Handle(Func handler) - { - Mappers.Add(handler); - return this; - } - - public TurboEntityAskBuilder Produces(Func handler) - { - Mappers.Add(async (ctx, resp) => await handler(ctx, resp).ExecuteAsync(ctx)); - return this; - } - - public TurboEntityAskBuilder WithTimeout(TimeSpan timeout) - { - TimeoutOverride = timeout; - return this; - } -} diff --git a/src/TurboHTTP/Server/TurboEntityBuilder.cs b/src/TurboHTTP/Server/TurboEntityBuilder.cs deleted file mode 100644 index 8bbe0ee1d..000000000 --- a/src/TurboHTTP/Server/TurboEntityBuilder.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Akka.Actor; -using Akka.Hosting; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; -using TurboHTTP.Routing.Binding; - -namespace TurboHTTP.Server; - -public sealed class TurboEntityBuilder -{ - private readonly string _pattern; - private readonly Dictionary _methods = new(StringComparer.OrdinalIgnoreCase); - private readonly EntityResponseMapperCollection _responseMappers = new(); - private TimeSpan _timeout = TimeSpan.FromSeconds(5); - private IEntityActorResolver _resolver = new GenericActorRefFactory(_ => ActorRefs.Nobody); - - public TurboEntityBuilder(string pattern) - { - _pattern = pattern; - } - - public TurboEntityMethodBuilder OnGet(Delegate messageFactory) - => AddMethod("GET", messageFactory); - - public TurboEntityMethodBuilder OnPost(Delegate messageFactory) - => AddMethod("POST", messageFactory); - - public TurboEntityMethodBuilder OnPut(Delegate messageFactory) - => AddMethod("PUT", messageFactory); - - public TurboEntityMethodBuilder OnDelete(Delegate messageFactory) - => AddMethod("DELETE", messageFactory); - - public TurboEntityMethodBuilder OnPatch(Delegate messageFactory) - => AddMethod("PATCH", messageFactory); - - public TurboEntityBuilder Response(Func mapper) - { - _responseMappers.Add(mapper); - return this; - } - - public TurboEntityBuilder WithTimeout(TimeSpan timeout) - { - _timeout = timeout; - return this; - } - - public TurboEntityBuilder UseResolver(IEntityActorResolver resolver) - { - _resolver = resolver; - return this; - } - - public TurboEntityBuilder UseResolver() where TResolver : IEntityActorResolver, new() - => UseResolver(new TResolver()); - - public TurboEntityBuilder UseActorRef() - => UseActorRef(x => x.Get()); - - public TurboEntityBuilder UseActorRef(Func factory) - => UseResolver(new GenericActorRefFactory(factory)); - - public TurboEntityBuilder UseActorRef(Func actorRefFactory) - => UseActorRef(sp => actorRefFactory(sp.GetRequiredService())); - - internal void AddToRouteTable(TurboRouteTable table) - { - foreach (var kv in _methods) - { - var methodConfig = kv.Value.ToConfig(); - var dispatcher = new EntityDispatcher( - methodConfig, - _responseMappers, - _timeout, - _resolver); - table.AddWithDispatcher(kv.Key, _pattern, dispatcher); - } - } - - private TurboEntityMethodBuilder AddMethod(string method, Delegate messageFactory) - { - var bound = DelegateHandlerBinder.BindEntityDelegate(_pattern, messageFactory); - var builder = new TurboEntityMethodBuilder(bound); - _methods[method] = builder; - return builder; - } - - private record GenericActorRefFactory(Func Factory) : IEntityActorResolver - { - public ValueTask ResolveAsync(IServiceProvider services, CancellationToken cancellationToken) - => ValueTask.FromResult(Factory(services)); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboEntityMethodBuilder.cs b/src/TurboHTTP/Server/TurboEntityMethodBuilder.cs deleted file mode 100644 index f972e8c09..000000000 --- a/src/TurboHTTP/Server/TurboEntityMethodBuilder.cs +++ /dev/null @@ -1,63 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public sealed class TurboEntityMethodBuilder -{ - private Func> MessageFactory { get; } - private bool _isTell; - private TimeSpan? _timeoutOverride; - private EntityResponseMapperCollection? _endpointMappers; - private Func? _tellResponseHandler; - - internal TurboEntityMethodBuilder(Func> messageFactory) - { - MessageFactory = messageFactory; - } - - public void Ask(Action configure) - { - _isTell = false; - _endpointMappers = null; - _tellResponseHandler = null; - - var builder = new TurboEntityAskBuilder(); - configure(builder); - - if (builder.Mappers.Count == 0) - { - throw new InvalidOperationException( - "IsAsk requires at least one Response or Produces handler."); - } - - _endpointMappers = builder.Mappers; - _timeoutOverride = builder.TimeoutOverride ?? _timeoutOverride; - } - - public void Tell(Action? configure = null) - { - _isTell = true; - _endpointMappers = null; - - if (configure is not null) - { - var builder = new TurboEntityTellBuilder(); - configure(builder); - _tellResponseHandler = builder.ResponseHandler; - } - } - - public TurboEntityMethodBuilder WithTimeout(TimeSpan timeout) - { - _timeoutOverride = timeout; - return this; - } - - internal EntityMethodConfig ToConfig() - => new( - MessageFactory, - _isTell, - _timeoutOverride, - _endpointMappers, - _tellResponseHandler); -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboHttpContext.cs b/src/TurboHTTP/Server/TurboHttpContext.cs deleted file mode 100644 index 8166bae00..000000000 --- a/src/TurboHTTP/Server/TurboHttpContext.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Security.Claims; -using Akka.Streams; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using TurboHTTP.Context; -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public sealed class TurboHttpContext : HttpContext -{ - private static readonly ClaimsPrincipal AnonymousPrincipal = new(); - - private IFeatureCollection _features; - private TurboConnectionInfo _connectionInfo; - private ClaimsPrincipal? _user; - private IDictionary? _items; - private string? _traceIdentifier; - private TurboEndpointMetadata? _endpointMetadata; - - public TurboHttpContext( - IFeatureCollection features, - TurboConnectionInfo connectionInfo, - IServiceProvider? services, - CancellationToken requestAborted, - IMaterializer materializer) - { - _features = features; - _connectionInfo = connectionInfo; - RequestServices = services!; - RequestAborted = requestAborted; - Materializer = materializer; - - TurboRequest = new TurboHttpRequest(features); - TurboRequest.SetHttpContext(this); - TurboResponse = new TurboHttpResponse(features); - TurboResponse.SetHttpContext(this); - } - - internal TurboHttpContext(IFeatureCollection features) - : this( - features, - new TurboConnectionInfo(Guid.NewGuid().ToString("N"), null, 0, null, 0), - services: null, - requestAborted: CancellationToken.None, - materializer: null!) - { - } - - public override IFeatureCollection Features => _features; - - public override HttpRequest Request => TurboRequest; - public TurboHttpRequest TurboRequest { get; } - - public override HttpResponse Response => TurboResponse; - public TurboHttpResponse TurboResponse { get; } - public override ConnectionInfo Connection => _connectionInfo; - public override WebSocketManager WebSockets - => throw new NotSupportedException( - "TurboHTTP does not support WebSockets. Use Akka.Streams for bidirectional streaming."); - - public override ClaimsPrincipal User - { - get => _user ?? AnonymousPrincipal; - set => _user = value; - } - - public override IDictionary Items - { - get => _items ??= new Dictionary(); - set => _items = value; - } - - public override IServiceProvider RequestServices { get; set; } - public override CancellationToken RequestAborted { get; set; } - - public override string TraceIdentifier - { - get => _traceIdentifier ??= Guid.NewGuid().ToString("N"); - set => _traceIdentifier = value; - } - - public override ISession Session - { - get => throw new NotSupportedException( - "TurboHTTP does not support ASP.NET Core sessions. Use ITurboMiddleware with context.Items for per-request state."); - set => throw new NotSupportedException( - "TurboHTTP does not support ASP.NET Core sessions. Use ITurboMiddleware with context.Items for per-request state."); - } - - public override void Abort() => RequestAborted = new CancellationToken(true); - - public IMaterializer Materializer { get; set; } = null!; - - internal TurboEndpointMetadata? EndpointMetadata - { - get => _endpointMetadata; - set => _endpointMetadata = value; - } - - internal void Reset( - IFeatureCollection features, - TurboConnectionInfo connectionInfo, - IServiceProvider? services, - CancellationToken requestAborted, - IMaterializer materializer) - { - _features = features; - _connectionInfo = connectionInfo; - _user = null; - _items = null; - _traceIdentifier = null; - _endpointMetadata = null; - RequestAborted = requestAborted; - RequestServices = services!; - Materializer = materializer; - - TurboRequest.Reset(features); - TurboRequest.SetHttpContext(this); - TurboResponse.Reset(features); - TurboResponse.SetHttpContext(this); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboHttpContextExtensions.cs b/src/TurboHTTP/Server/TurboHttpContextExtensions.cs deleted file mode 100644 index 817726c7c..000000000 --- a/src/TurboHTTP/Server/TurboHttpContextExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public static class TurboHttpContextExtensions -{ - public static TurboEndpointMetadata? GetEndpointMetadata(this TurboHttpContext context) - => context.EndpointMetadata; - - public static bool HasEndpointMetadata(this TurboHttpContext context) where T : class - => context.EndpointMetadata?.HasMetadata() == true; -} diff --git a/src/TurboHTTP/Server/TurboMiddlewareExtensions.cs b/src/TurboHTTP/Server/TurboMiddlewareExtensions.cs deleted file mode 100644 index adef54c00..000000000 --- a/src/TurboHTTP/Server/TurboMiddlewareExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Server; - -public static class TurboMiddlewareExtensions -{ - [Obsolete("Use TurboWebApplication with Use() instead. Will be removed in 2.0.")] - public static WebApplication UseTurbo( - this WebApplication app, - Func middleware) - { - app.Services.GetRequiredService() - .Use(middleware); - return app; - } - - [Obsolete("Use TurboWebApplication with Use() instead. Will be removed in 2.0.")] - public static WebApplication UseTurbo(this WebApplication app) - where T : class, ITurboMiddleware - { - app.Services.GetRequiredService() - .Use(); - return app; - } - - [Obsolete("Use TurboWebApplication with Run() instead. Will be removed in 2.0.")] - public static WebApplication RunTurbo( - this WebApplication app, - TurboRequestDelegate handler) - { - app.Services.GetRequiredService() - .Run(handler); - return app; - } - - [Obsolete("Use TurboWebApplication with Map() instead. Will be removed in 2.0.")] - public static WebApplication MapTurbo( - this WebApplication app, - string pathPrefix, - Action configure) - { - app.Services.GetRequiredService() - .Map(pathPrefix, configure); - return app; - } - - [Obsolete("Use TurboWebApplication with MapWhen() instead. Will be removed in 2.0.")] - public static WebApplication MapTurboWhen( - this WebApplication app, - Func predicate, - Action configure) - { - app.Services.GetRequiredService() - .MapWhen(predicate, configure); - return app; - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboRequestDelegate.cs b/src/TurboHTTP/Server/TurboRequestDelegate.cs deleted file mode 100644 index 13144071e..000000000 --- a/src/TurboHTTP/Server/TurboRequestDelegate.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace TurboHTTP.Server; - -public delegate Task TurboRequestDelegate(TurboHttpContext context); diff --git a/src/TurboHTTP/Server/TurboRouteGroupBuilder.cs b/src/TurboHTTP/Server/TurboRouteGroupBuilder.cs deleted file mode 100644 index 0c573e2e9..000000000 --- a/src/TurboHTTP/Server/TurboRouteGroupBuilder.cs +++ /dev/null @@ -1,136 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public sealed class TurboRouteGroupBuilder -{ - private readonly string _prefix; - private readonly TurboRouteTable _table; - private readonly List _groupMetadata = []; - private readonly List _tags = []; - - internal TurboRouteGroupBuilder(string prefix, TurboRouteTable table) - { - _prefix = prefix; - _table = table; - } - - private TurboRouteGroupBuilder(string prefix, TurboRouteTable table, List parentMetadata, List parentTags) - { - _prefix = prefix; - _table = table; - _groupMetadata.AddRange(parentMetadata); - _tags.AddRange(parentTags); - } - - public TurboRouteHandlerBuilder MapGet(string pattern, Delegate handler) - { - var builder = _table.Add("GET", _prefix + pattern, handler); - ApplyGroupMetadata(builder); - return builder; - } - - public TurboRouteHandlerBuilder MapPost(string pattern, Delegate handler) - { - var builder = _table.Add("POST", _prefix + pattern, handler); - ApplyGroupMetadata(builder); - return builder; - } - - public TurboRouteHandlerBuilder MapPut(string pattern, Delegate handler) - { - var builder = _table.Add("PUT", _prefix + pattern, handler); - ApplyGroupMetadata(builder); - return builder; - } - - public TurboRouteHandlerBuilder MapDelete(string pattern, Delegate handler) - { - var builder = _table.Add("DELETE", _prefix + pattern, handler); - ApplyGroupMetadata(builder); - return builder; - } - - public TurboRouteHandlerBuilder MapPatch(string pattern, Delegate handler) - { - var builder = _table.Add("PATCH", _prefix + pattern, handler); - ApplyGroupMetadata(builder); - return builder; - } - - public TurboRouteHandlerBuilder MapMethods(string pattern, IEnumerable methods, Delegate handler) - { - TurboRouteHandlerBuilder? last = null; - foreach (var method in methods) - { - last = _table.Add(method, _prefix + pattern, handler); - ApplyGroupMetadata(last); - } - - return last!; - } - - private void ApplyGroupMetadata(TurboRouteHandlerBuilder builder) - { - if (_tags.Count > 0) - { - builder.WithTags(_tags.ToArray()); - } - - foreach (var item in _groupMetadata) - { - builder.WithMetadata(item); - } - } - - public TurboRouteGroupBuilder MapGroup(string prefix) - { - return new TurboRouteGroupBuilder(_prefix + prefix, _table, _groupMetadata, _tags); - } - - public TurboRouteGroupBuilder WithTags(params string[] tags) - { - _tags.AddRange(tags); - return this; - } - - public TurboRouteGroupBuilder WithMetadata(params object[] metadata) - { - _groupMetadata.AddRange(metadata); - return this; - } - - public TurboRouteGroupBuilder RequireAuthorization() - { - _groupMetadata.Add(new AuthorizeData(null, null, null)); - return this; - } - - public TurboRouteGroupBuilder RequireAuthorization(string? policy) - { - _groupMetadata.Add(new AuthorizeData(policy, null, null)); - return this; - } - - public TurboRouteGroupBuilder AllowAnonymous() - { - _groupMetadata.Add(new AllowAnonymousMarker()); - return this; - } - - public TurboRouteHandlerBuilder MapEntity(string pattern, Action configure) - { - var entityBuilder = new TurboEntityBuilder(_prefix + pattern); - configure(entityBuilder); - entityBuilder.AddToRouteTable(_table); - return new TurboRouteHandlerBuilder(); - } - - public TurboRouteHandlerBuilder MapEntity(string pattern, Action configure) - { - var entityBuilder = new TurboEntityBuilder(_prefix + pattern).UseActorRef(x => x.Get()); - configure(entityBuilder); - entityBuilder.AddToRouteTable(_table); - return new TurboRouteHandlerBuilder(); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboRouteHandlerBuilder.cs b/src/TurboHTTP/Server/TurboRouteHandlerBuilder.cs deleted file mode 100644 index 37b2dc22c..000000000 --- a/src/TurboHTTP/Server/TurboRouteHandlerBuilder.cs +++ /dev/null @@ -1,101 +0,0 @@ -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - - -public sealed class TurboRouteHandlerBuilder -{ - public EndpointMetadata Metadata { get; } = new(); - - public TurboRouteHandlerBuilder WithName(string name) - { - Metadata.Name = name; - return this; - } - - public TurboRouteHandlerBuilder WithTags(params string[] tags) - { - Metadata.Tags.AddRange(tags); - return this; - } - - public TurboRouteHandlerBuilder WithMetadata(params object[] metadata) - { - Metadata.Items.AddRange(metadata); - return this; - } - - public TurboRouteHandlerBuilder RequireAuthorization() - { - Metadata.RequiresAuthorization = true; - return this; - } - - public TurboRouteHandlerBuilder RequireAuthorization(string? policy) - { - Metadata.AuthorizationPolicies.Add(policy); - return this; - } - - public TurboRouteHandlerBuilder AllowAnonymous() - { - Metadata.AllowsAnonymous = true; - return this; - } - - public TurboRouteHandlerBuilder WithDisplayName(string displayName) - { - Metadata.DisplayName = displayName; - return this; - } - - public TurboRouteHandlerBuilder Produces(int statusCode = 200) - { - Metadata.Items.Add(new ProducesMetadata(typeof(T), statusCode)); - return this; - } - - public TurboRouteHandlerBuilder ProducesProblem(int statusCode = 500) - { - Metadata.Items.Add(new ProducesProblemMetadata(statusCode)); - return this; - } - - internal TurboEndpointMetadata? BuildMetadata() - { - if (Metadata.Items.Count == 0 && Metadata.Tags.Count == 0 && - !Metadata.RequiresAuthorization && !Metadata.AllowsAnonymous && - Metadata.AuthorizationPolicies.Count == 0 && - Metadata.Name is null && Metadata.DisplayName is null) - { - return null; - } - - var items = new List(Metadata.Items); - - if (Metadata.Tags.Count > 0) - { - items.Add(new TagsMetadata(Metadata.Tags.ToArray())); - } - - foreach (var policy in Metadata.AuthorizationPolicies) - { - items.Add(new AuthorizeData(policy, null, null)); - } - - if (Metadata.RequiresAuthorization && Metadata.AuthorizationPolicies.Count == 0) - { - items.Add(new AuthorizeData(null, null, null)); - } - - if (Metadata.AllowsAnonymous) - { - items.Add(new AllowAnonymousMarker()); - } - - return new TurboEndpointMetadata(items); - } -} - -public sealed record ProducesMetadata(Type Type, int StatusCode); -public sealed record ProducesProblemMetadata(int StatusCode); diff --git a/src/TurboHTTP/Server/TurboRoutingExtensions.cs b/src/TurboHTTP/Server/TurboRoutingExtensions.cs deleted file mode 100644 index 7e036a2f8..000000000 --- a/src/TurboHTTP/Server/TurboRoutingExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using TurboHTTP.Routing; - -namespace TurboHTTP.Server; - -public static class TurboRoutingExtensions -{ - [Obsolete("Use TurboWebApplication with MapGet() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboGet(this WebApplication app, string pattern, Delegate handler) - { - return app.Services.GetRequiredService().Add("GET", pattern, handler); - } - - [Obsolete("Use TurboWebApplication with MapPost() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboPost(this WebApplication app, string pattern, Delegate handler) - { - return app.Services.GetRequiredService().Add("POST", pattern, handler); - } - - [Obsolete("Use TurboWebApplication with MapPut() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboPut(this WebApplication app, string pattern, Delegate handler) - { - return app.Services.GetRequiredService().Add("PUT", pattern, handler); - } - - [Obsolete("Use TurboWebApplication with MapDelete() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboDelete(this WebApplication app, string pattern, Delegate handler) - { - return app.Services.GetRequiredService().Add("DELETE", pattern, handler); - } - - [Obsolete("Use TurboWebApplication with MapPatch() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboPatch(this WebApplication app, string pattern, Delegate handler) - { - return app.Services.GetRequiredService().Add("PATCH", pattern, handler); - } - - [Obsolete("Use TurboWebApplication with MapMethods() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboMethods( - this WebApplication app, string pattern, IEnumerable methods, Delegate handler) - { - TurboRouteHandlerBuilder? last = null; - foreach (var method in methods) - { - last = app.Services.GetRequiredService().Add(method, pattern, handler); - } - - return last!; - } - - [Obsolete("Use TurboWebApplication with MapGroup() instead. Will be removed in 2.0.")] - public static TurboRouteGroupBuilder MapTurboGroup(this WebApplication app, string prefix) - { - return app.Services.GetRequiredService().CreateGroup(prefix); - } - - [Obsolete("Use TurboWebApplication with MapEntity() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboEntity(this WebApplication app, string pattern, - Action configure) - { - var entityBuilder = new TurboEntityBuilder(pattern); - configure(entityBuilder); - entityBuilder.AddToRouteTable(app.Services.GetRequiredService()); - return new TurboRouteHandlerBuilder(); - } - - [Obsolete("Use TurboWebApplication with MapEntity() instead. Will be removed in 2.0.")] - public static TurboRouteHandlerBuilder MapTurboEntity(this WebApplication app, string pattern, - Action configure) - { - var entityBuilder = new TurboEntityBuilder(pattern).UseActorRef(x => x.Get()); - configure(entityBuilder); - entityBuilder.AddToRouteTable(app.Services.GetRequiredService()); - return new TurboRouteHandlerBuilder(); - } - -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/Hosting/TurboServerHostedService.cs b/src/TurboHTTP/Server/TurboServer.cs similarity index 61% rename from src/TurboHTTP/Server/Hosting/TurboServerHostedService.cs rename to src/TurboHTTP/Server/TurboServer.cs index 29caf0d49..1870aeacc 100644 --- a/src/TurboHTTP/Server/Hosting/TurboServerHostedService.cs +++ b/src/TurboHTTP/Server/TurboServer.cs @@ -3,45 +3,48 @@ using Akka.Configuration; using Akka.Hosting.Logging; using Akka.Streams; +using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using TurboHTTP.Routing; -using TurboHTTP.Server.Middleware; +using Microsoft.Extensions.Options; +using Servus.Akka.Transport; using TurboHTTP.Streams.Lifecycle; +using TurboHTTP.Streams.Stages.Server; -namespace TurboHTTP.Server.Hosting; +namespace TurboHTTP.Server; -internal sealed class TurboServerHostedService : IHostedService, IDisposable +public sealed class TurboServer : IServer { private static readonly Config LoggingHocon = ConfigurationFactory.ParseString( """akka.loggers = ["Akka.Hosting.Logging.LoggerFactoryLogger, Akka.Hosting"]"""); private readonly TurboServerOptions _options; - private readonly TurboRouteTable _routeTable; - private readonly TurboPipelineBuilder _pipelineBuilder; - private readonly IServiceProvider _services; private readonly ILoggerFactory _loggerFactory; + private readonly IServiceProvider _services; + private readonly FeatureCollection _features = new(); private ActorSystem? _system; private bool _ownsSystem; private IActorRef _supervisor = ActorRefs.Nobody; - public TurboServerHostedService( - TurboServerOptions options, - TurboRouteTable routeTable, - TurboPipelineBuilder pipelineBuilder, - IServiceProvider services, - ILoggerFactory loggerFactory) + public TurboServer(IOptions options, ILoggerFactory loggerFactory, IServiceProvider services) { - _options = options; - _routeTable = routeTable; - _pipelineBuilder = pipelineBuilder; - _services = services; + _options = options.Value; _loggerFactory = loggerFactory; + _services = services; + + var addressesFeature = new ServerAddressesFeature(); + _features.Set(addressesFeature); } - public async Task StartAsync(CancellationToken cancellationToken) + public IFeatureCollection Features => _features; + + public async Task StartAsync( + IHttpApplication application, + CancellationToken cancellationToken) where TContext : notnull { _system = _services.GetService(); if (_system is null) @@ -54,12 +57,30 @@ public async Task StartAsync(CancellationToken cancellationToken) } var materializer = _system.Materializer(); - var routeTable = _routeTable.Freeze(); - var pipeline = _pipelineBuilder.Build(); + + var parallelism = _options.Http2.MaxConcurrentStreams; + var bridgeFlow = Flow.FromGraph(new ApplicationBridgeStage( + application, + parallelism, + _options.HandlerTimeout, + _options.HandlerGracePeriod)); var resolver = new EndpointResolver(); var resolvedEndpoints = resolver.Resolve(_options); + var addressesFeature = _features.Get()!; + foreach (var endpoint in resolvedEndpoints) + { + var opts = endpoint.Options; + var scheme = (opts is TcpListenerOptions tcp && tcp.ServerCertificate is not null) ? "https" : "http"; + var host = opts.Host ?? "localhost"; + if (host == "0.0.0.0" || host == "::") + { + host = "localhost"; + } + addressesFeature.Addresses.Add(string.Concat(scheme, "://", host, ":", opts.Port.ToString())); + } + var listenerProps = new List(resolvedEndpoints.Count); foreach (var endpoint in resolvedEndpoints) { @@ -67,8 +88,7 @@ public async Task StartAsync(CancellationToken cancellationToken) endpoint.Factory, endpoint.Options, _options, - pipeline, - routeTable, + bridgeFlow, _services, materializer, endpoint.ConnectionLoggingCategory)); @@ -102,7 +122,6 @@ public async Task StartAsync(CancellationToken cancellationToken) await Task.Delay(_options.GracefulShutdownTimeout, CancellationToken.None); return Done.Instance; }); - } public async Task StopAsync(CancellationToken cancellationToken) @@ -121,3 +140,9 @@ public void Dispose() } } } + +internal sealed class ServerAddressesFeature : IServerAddressesFeature +{ + public ICollection Addresses { get; } = new List(); + public bool PreferHostingUrls { get; set; } +} diff --git a/src/TurboHTTP/Server/TurboServerOptions.cs b/src/TurboHTTP/Server/TurboServerOptions.cs index 7e50bc8cb..e864e55f1 100644 --- a/src/TurboHTTP/Server/TurboServerOptions.cs +++ b/src/TurboHTTP/Server/TurboServerOptions.cs @@ -2,8 +2,6 @@ using Servus.Akka.Transport; using Servus.Akka.Transport.Tcp.Listener; using Servus.Akka.Transport.Quic.Listener; -using TurboHTTP.Routing; -using TurboHTTP.Server.Middleware; namespace TurboHTTP.Server; @@ -11,33 +9,6 @@ public sealed class TurboServerOptions { public TurboServerLimits Limits { get; } = new(); - [Obsolete("Use Limits.MaxConcurrentConnections instead")] - public int MaxConcurrentConnections - { - get => Limits.MaxConcurrentConnections; - set => Limits.MaxConcurrentConnections = value; - } - - [Obsolete("Use Limits.MaxConcurrentUpgradedConnections instead")] - public int MaxConcurrentUpgradedConnections - { - get => Limits.MaxConcurrentUpgradedConnections; - set => Limits.MaxConcurrentUpgradedConnections = value; - } - - [Obsolete("Use Limits.KeepAliveTimeout instead")] - public TimeSpan KeepAliveTimeout - { - get => Limits.KeepAliveTimeout; - set => Limits.KeepAliveTimeout = value; - } - - [Obsolete("Use Limits.RequestHeadersTimeout instead")] - public TimeSpan RequestHeadersTimeout - { - get => Limits.RequestHeadersTimeout; - set => Limits.RequestHeadersTimeout = value; - } public TimeSpan GracefulShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30); public TimeSpan HandlerTimeout { get; set; } = TimeSpan.FromSeconds(30); public TimeSpan HandlerGracePeriod { get; set; } = TimeSpan.FromSeconds(5); @@ -62,7 +33,7 @@ public void Bind(QuicListenerOptions options) Endpoints.Add(new ListenerBinding { Options = options, Factory = new QuicListenerFactory() }); } - public void BindTcp(string host, ushort port) => Bind(new TcpListenerOptions() { Host = host, Port = port }); + public void BindTcp(string host, ushort port) => Bind(new TcpListenerOptions { Host = host, Port = port }); internal IList ListenOptions { get; } = new List(); internal Action? HttpsDefaultsCallback { get; private set; } diff --git a/src/TurboHTTP/Server/TurboServerServiceCollectionExtensions.cs b/src/TurboHTTP/Server/TurboServerServiceCollectionExtensions.cs deleted file mode 100644 index dde229854..000000000 --- a/src/TurboHTTP/Server/TurboServerServiceCollectionExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using TurboHTTP.Routing; -using TurboHTTP.Server.Hosting; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Server; - -public static class TurboServerServiceCollectionExtensions -{ - public static IServiceCollection AddTurboKestrel( - this IServiceCollection services, - Action? configure = null) - { - var options = new TurboServerOptions(); - configure?.Invoke(options); - - services.TryAddSingleton(options); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return services; - } - - public static IServiceCollection AddTurboKestrel( - this IServiceCollection services, - IConfiguration configuration, - Action? configure = null) - { - var options = new TurboServerOptions(); - TurboKestrelConfigurationBinder.Bind(options, configuration.GetSection("TurboKestrel")); - configure?.Invoke(options); - - services.TryAddSingleton(options); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return services; - } - - internal static IServiceCollection AddTurboKestrel( - this IServiceCollection services, - TurboServerOptions options) - { - services.TryAddSingleton(options); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return services; - } -} diff --git a/src/TurboHTTP/Server/TurboServerWebHostBuilderExtensions.cs b/src/TurboHTTP/Server/TurboServerWebHostBuilderExtensions.cs new file mode 100644 index 000000000..531b6f687 --- /dev/null +++ b/src/TurboHTTP/Server/TurboServerWebHostBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; + +namespace TurboHTTP.Server; + +public static class TurboServerWebHostBuilderExtensions +{ + public static IHostBuilder UseTurboHttp( + this IHostBuilder builder, + Action? configure = null) + { + builder.ConfigureServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + if (configure is not null) + { + services.Configure(configure); + } + }); + return builder; + } +} diff --git a/src/TurboHTTP/Server/TurboStreamResults.cs b/src/TurboHTTP/Server/TurboStreamResults.cs deleted file mode 100644 index 34e1d04b6..000000000 --- a/src/TurboHTTP/Server/TurboStreamResults.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Text; -using Akka; -using Akka.Streams.Dsl; -using Microsoft.AspNetCore.Http; -using TurboHTTP.Context.Features; -using TurboHTTP.Features.Sse; - -namespace TurboHTTP.Server; - -public static class TurboStreamResults -{ - public static IResult EventStream(Source source) - { - return new EventStreamResult(source); - } - - public static IResult EventStream(Source source) - { - return new SseEventStreamResult(source); - } - - public static IResult Stream(Source, NotUsed> source, string? contentType = null) - { - return new AkkaStreamResult(source, contentType); - } -} - -internal sealed class EventStreamResult(Source source) : IResult -{ - public async Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = 200; - httpContext.Response.ContentType = "text/event-stream"; - httpContext.Response.Headers.CacheControl = "no-cache"; - - if (httpContext is not TurboHttpContext turboCtx) - { - return; - } - - if (httpContext.Features.Get() is not TurboHttpResponseBodyFeature bodyFeature) - { - return; - } - - var byteSource = source.Select(text => - { - var formatted = string.Concat("data: ", text, "\n\n"); - return (ReadOnlyMemory)Encoding.UTF8.GetBytes(formatted).AsMemory(); - }); - - await byteSource.RunWith(bodyFeature.BodySink, turboCtx.Materializer); - await bodyFeature.CompleteAsync(); - } -} - -internal sealed class SseEventStreamResult(Source source) : IResult -{ - public async Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = 200; - httpContext.Response.ContentType = "text/event-stream"; - httpContext.Response.Headers.CacheControl = "no-cache"; - - if (httpContext is not TurboHttpContext turboCtx) - { - return; - } - - if (httpContext.Features.Get() is not TurboHttpResponseBodyFeature bodyFeature) - { - return; - } - - var byteSource = source.Via(SseFormatterFlow.Instance); - - await byteSource.RunWith(bodyFeature.BodySink, turboCtx.Materializer); - await bodyFeature.CompleteAsync(); - } -} - -internal sealed class AkkaStreamResult(Source, NotUsed> source, string? contentType) : IResult -{ - public async Task ExecuteAsync(HttpContext httpContext) - { - httpContext.Response.StatusCode = 200; - if (contentType is not null) - { - httpContext.Response.ContentType = contentType; - } - - if (httpContext is not TurboHttpContext turboCtx) - { - return; - } - - if (httpContext.Features.Get() is not TurboHttpResponseBodyFeature bodyFeature) - { - return; - } - - await source.RunWith(bodyFeature.BodySink, turboCtx.Materializer); - await bodyFeature.CompleteAsync(); - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Server/TurboUrlCollection.cs b/src/TurboHTTP/Server/TurboUrlCollection.cs deleted file mode 100644 index 6e9992549..000000000 --- a/src/TurboHTTP/Server/TurboUrlCollection.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections; - -namespace TurboHTTP.Server; - -internal sealed class TurboUrlCollection : ICollection -{ - private readonly TurboServerOptions _options; - - internal TurboUrlCollection(TurboServerOptions options) - { - _options = options; - } - - public int Count => _options.Urls.Count; - - public bool IsReadOnly => false; - - public void Add(string url) - { - _options.Urls.Add(url); - } - - public bool Remove(string url) - { - return _options.Urls.Remove(url); - } - - public void Clear() - { - _options.Urls.Clear(); - } - - public bool Contains(string url) - { - return _options.Urls.Contains(url); - } - - public void CopyTo(string[] array, int arrayIndex) - { - _options.Urls.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return _options.Urls.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } -} diff --git a/src/TurboHTTP/Server/TurboWebApplication.cs b/src/TurboHTTP/Server/TurboWebApplication.cs deleted file mode 100644 index 355cef6f7..000000000 --- a/src/TurboHTTP/Server/TurboWebApplication.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TurboHTTP.Routing; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Server; - -public sealed class TurboWebApplication : IHost, ITurboEndpointRouteBuilder, ITurboApplicationBuilder, IAsyncDisposable, IRouteTableAccessor -{ - private readonly IHost _host; - private readonly TurboRouteTable _routeTable; - private readonly TurboPipelineBuilder _pipelineBuilder; - private readonly TurboUrlCollection _urls; - - internal TurboWebApplication(IHost host, TurboRouteTable routeTable, TurboPipelineBuilder pipelineBuilder) - { - _host = host ?? throw new ArgumentNullException(nameof(host)); - _routeTable = routeTable ?? throw new ArgumentNullException(nameof(routeTable)); - _pipelineBuilder = pipelineBuilder ?? throw new ArgumentNullException(nameof(pipelineBuilder)); - - var options = host.Services.GetRequiredService(); - _urls = new TurboUrlCollection(options); - - Logger = host.Services.GetRequiredService() - .CreateLogger(Environment.ApplicationName ?? nameof(TurboWebApplication)); - } - - public IServiceProvider Services => _host.Services; - - public IConfiguration Configuration => Services.GetRequiredService(); - - public IHostEnvironment Environment => Services.GetRequiredService(); - - public IHostApplicationLifetime Lifetime => Services.GetRequiredService(); - - public ILogger Logger { get; } - - public ICollection Urls => _urls; - - public void Dispose() - { - _host.Dispose(); - } - - public async ValueTask DisposeAsync() - { - if (_host is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else - { - _host.Dispose(); - } - } - - public Task StartAsync(CancellationToken cancellationToken = default) - { - return _host.StartAsync(cancellationToken); - } - - public Task StopAsync(CancellationToken cancellationToken = default) - { - return _host.StopAsync(cancellationToken); - } - - public Task RunAsync(CancellationToken cancellationToken = default) - { - return _host.RunAsync(cancellationToken); - } - - public async Task RunAsync(TimeSpan timeout, CancellationToken cancellationToken = default) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(timeout); - await _host.RunAsync(cts.Token); - } - - public Task WaitForShutdownAsync(CancellationToken cancellationToken = default) - { - return _host.WaitForShutdownAsync(cancellationToken); - } - - IServiceProvider ITurboEndpointRouteBuilder.ServiceProvider => Services; - - TurboRouteTable ITurboEndpointRouteBuilder.RouteTable => _routeTable; - - TurboRouteTable IRouteTableAccessor.RouteTable => _routeTable; - - public ITurboApplicationBuilder Use(Func middleware) - { - _pipelineBuilder.Use(middleware); - return this; - } - - public ITurboApplicationBuilder Use() where T : class, ITurboMiddleware - { - _pipelineBuilder.Use(); - return this; - } - - public ITurboApplicationBuilder Run(TurboRequestDelegate handler) - { - _pipelineBuilder.Run(handler); - return this; - } - - public ITurboApplicationBuilder Map(string pathPrefix, Action configure) - { - _pipelineBuilder.Map(pathPrefix, configure); - return this; - } - - public ITurboApplicationBuilder MapWhen(Func predicate, Action configure) - { - _pipelineBuilder.MapWhen(predicate, configure); - return this; - } - - public static TurboWebApplicationBuilder CreateBuilder() - { - return new TurboWebApplicationBuilder(null); - } - - public static TurboWebApplicationBuilder CreateBuilder(string[] args) - { - return new TurboWebApplicationBuilder(args); - } - - public static TurboWebApplication Create(string[]? args = null) - { - return new TurboWebApplicationBuilder(args).Build(); - } -} diff --git a/src/TurboHTTP/Server/TurboWebApplicationBuilder.cs b/src/TurboHTTP/Server/TurboWebApplicationBuilder.cs deleted file mode 100644 index d476562b6..000000000 --- a/src/TurboHTTP/Server/TurboWebApplicationBuilder.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TurboHTTP.Routing; -using TurboHTTP.Server.Middleware; - -namespace TurboHTTP.Server; - -public sealed class TurboHostBuilder -{ - private readonly HostApplicationBuilder _inner; - - internal TurboHostBuilder(HostApplicationBuilder inner) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } - - public TurboHostBuilder ConfigureHostOptions(Action configure) - { - _inner.Services.Configure(configure); - return this; - } - - public TurboHostBuilder ConfigureAppConfiguration(Action configure) - { - configure(_inner.Configuration); - return this; - } - - public TurboHostBuilder ConfigureServices(Action configure) - { - configure(_inner.Services); - return this; - } -} - -public sealed class TurboWebApplicationBuilder -{ - private readonly HostApplicationBuilder _hostBuilder; - private readonly TurboHostBuilder _hostBuilderFacade; - - internal TurboWebApplicationBuilder(string[]? args) - { - _hostBuilder = global::Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(args ?? []); - _hostBuilderFacade = new TurboHostBuilder(_hostBuilder); - } - - public TurboHostBuilder Host => _hostBuilderFacade; - - public IServiceCollection Services => _hostBuilder.Services; - - public ConfigurationManager Configuration => _hostBuilder.Configuration; - - public ILoggingBuilder Logging => _hostBuilder.Logging; - - public IHostEnvironment Environment => _hostBuilder.Environment; - - public TurboServerOptions Server { get; } = new(); - - public TurboWebApplication Build() - { - Services.AddTurboKestrel(Server); - - var host = _hostBuilder.Build(); - var routeTable = host.Services.GetRequiredService(); - var pipelineBuilder = host.Services.GetRequiredService(); - - return new TurboWebApplication(host, routeTable, pipelineBuilder); - } -} diff --git a/src/TurboHTTP/Streams/FeaturePipelineBuilder.cs b/src/TurboHTTP/Streams/FeaturePipelineBuilder.cs index 962beabc8..2d7ae3570 100644 --- a/src/TurboHTTP/Streams/FeaturePipelineBuilder.cs +++ b/src/TurboHTTP/Streams/FeaturePipelineBuilder.cs @@ -1,4 +1,3 @@ -using TurboHTTP.Client; using Akka; using Akka.Streams; using Akka.Streams.Dsl; diff --git a/src/TurboHTTP/Streams/Http10ServerEngine.cs b/src/TurboHTTP/Streams/Http10ServerEngine.cs index e9aef9472..cbff0032f 100644 --- a/src/TurboHTTP/Streams/Http10ServerEngine.cs +++ b/src/TurboHTTP/Streams/Http10ServerEngine.cs @@ -1,6 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Server; using TurboHTTP.Streams.Stages.Server; @@ -16,7 +17,9 @@ public Http10ServerEngine(TurboServerOptions options) _options = options; } - public BidiFlow CreateFlow(IServiceProvider? services = null) + public Version ProtocolVersion => new(1, 0); + + public BidiFlow CreateFlow(IServiceProvider? services = null) { return BidiFlow.FromGraph(GraphDsl.Create(b => { @@ -24,8 +27,8 @@ public BidiFlow( connection.InNetwork, connection.OutRequest, diff --git a/src/TurboHTTP/Streams/Http11ServerEngine.cs b/src/TurboHTTP/Streams/Http11ServerEngine.cs index 473ebe7f5..a6f4c0d43 100644 --- a/src/TurboHTTP/Streams/Http11ServerEngine.cs +++ b/src/TurboHTTP/Streams/Http11ServerEngine.cs @@ -1,6 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Server; using TurboHTTP.Streams.Stages.Server; @@ -16,7 +17,9 @@ public Http11ServerEngine(TurboServerOptions options) _options = options; } - public BidiFlow CreateFlow(IServiceProvider? services = null) + public Version ProtocolVersion => new(1, 1); + + public BidiFlow CreateFlow(IServiceProvider? services = null) { return BidiFlow.FromGraph(GraphDsl.Create(b => { @@ -24,8 +27,8 @@ public BidiFlow( connection.InNetwork, connection.OutRequest, diff --git a/src/TurboHTTP/Streams/Http20ServerEngine.cs b/src/TurboHTTP/Streams/Http20ServerEngine.cs index db9e2a0b5..3de5a4239 100644 --- a/src/TurboHTTP/Streams/Http20ServerEngine.cs +++ b/src/TurboHTTP/Streams/Http20ServerEngine.cs @@ -1,6 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Server; using TurboHTTP.Streams.Stages.Server; @@ -16,7 +17,9 @@ public Http20ServerEngine(TurboServerOptions options) _options = options; } - public BidiFlow CreateFlow(IServiceProvider? services = null) + public Version ProtocolVersion => new(2, 0); + + public BidiFlow CreateFlow(IServiceProvider? services = null) { return BidiFlow.FromGraph(GraphDsl.Create(b => { @@ -24,8 +27,8 @@ public BidiFlow( connection.InNetwork, connection.OutRequest, diff --git a/src/TurboHTTP/Streams/Http30ServerEngine.cs b/src/TurboHTTP/Streams/Http30ServerEngine.cs index 8d2be0ae6..739ad0e7d 100644 --- a/src/TurboHTTP/Streams/Http30ServerEngine.cs +++ b/src/TurboHTTP/Streams/Http30ServerEngine.cs @@ -1,6 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Server; using TurboHTTP.Streams.Stages.Server; @@ -16,7 +17,9 @@ public Http30ServerEngine(TurboServerOptions options) _options = options; } - public BidiFlow CreateFlow(IServiceProvider? services = null) + public Version ProtocolVersion => new(3, 0); + + public BidiFlow CreateFlow(IServiceProvider? services = null) { return BidiFlow.FromGraph(GraphDsl.Create(b => { @@ -24,8 +27,8 @@ public BidiFlow( connection.InNetwork, connection.OutRequest, diff --git a/src/TurboHTTP/Streams/IServerProtocolEngine.cs b/src/TurboHTTP/Streams/IServerProtocolEngine.cs index 4c3e258f0..d3026ef75 100644 --- a/src/TurboHTTP/Streams/IServerProtocolEngine.cs +++ b/src/TurboHTTP/Streams/IServerProtocolEngine.cs @@ -1,13 +1,15 @@ using Akka; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Server; namespace TurboHTTP.Streams; internal interface IServerProtocolEngine { - BidiFlow CreateFlow( + Version ProtocolVersion { get; } + + BidiFlow CreateFlow( IServiceProvider? services = null); } diff --git a/src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs b/src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs index ddd6b5af3..2a3c5b2f2 100644 --- a/src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs +++ b/src/TurboHTTP/Streams/Lifecycle/ConnectionActor.cs @@ -1,17 +1,16 @@ -using System.Net.Security; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Akka; using Akka.Actor; using Akka.Event; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Servus.Akka.Transport; using TurboHTTP.Diagnostics; -using TurboHTTP.Routing; -using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; -using TurboHTTP.Streams.Stages.Server; +using static Servus.Core.Servus; namespace TurboHTTP.Streams.Lifecycle; @@ -30,24 +29,24 @@ internal sealed class ConnectionActor : ReceiveActor private SharedKillSwitch? _killSwitch; private bool _draining; private readonly CancellationTokenSource _cts = new(); + private long _connectionTimestamp; + private Activity? _connectionActivity; public sealed record Materialize( Flow ConnectionFlow, IServerProtocolEngine Engine, - TurboRequestDelegate Pipeline, - RouteTable RouteTable, - int Parallelism, + Flow BridgeFlow, IServiceProvider Services, IMaterializer Materializer, - TimeSpan HandlerTimeout, - TimeSpan HandlerGracePeriod, - string? ConnectionLoggingCategory = null); + string? ConnectionLoggingCategory = null, + long ConnectionTimestamp = 0, + Activity? ConnectionActivity = null); public sealed record GracefulStop(TimeSpan Timeout); public sealed record StreamCompleted(Exception? Error); - public sealed record ConnectionCompleted(string ConnectionId, ConnectionCompletionReason Reason); + public sealed record ConnectionCompleted(string ConnectionId, ConnectionCompletionReason Reason, long ConnectionTimestamp = 0, Activity? ConnectionActivity = null); public ConnectionActor(string connectionId) { @@ -61,13 +60,21 @@ public ConnectionActor(string connectionId) private void OnMaterialize(Materialize msg) { + _connectionTimestamp = msg.ConnectionTimestamp; + _connectionActivity = msg.ConnectionActivity; _log.Debug("Connection {0} materializing pipeline", _connectionId); + var negotiationStart = Stopwatch.GetTimestamp(); + _killSwitch = KillSwitches.Shared("connection-" + _connectionId); - var routing = Flow.FromGraph(new RoutingStage(msg.RouteTable, msg.Pipeline, msg.Parallelism, msg.HandlerTimeout, msg.HandlerGracePeriod)); var protocolBidi = msg.Engine.CreateFlow(msg.Services); - var composed = protocolBidi.Join(routing); + var composed = protocolBidi.Join(msg.BridgeFlow); + + if (Metrics.ProtocolNegotiationDuration().Enabled) + { + RecordProtocolNegotiation(negotiationStart, msg.Engine); + } var self = Self; Flow? loggingFlow = null; @@ -112,6 +119,16 @@ private void OnMaterialize(Materialize msg) failure: ex => new StreamCompleted(ex)); } + [MethodImpl(MethodImplOptions.NoInlining)] + private void RecordProtocolNegotiation(long startTimestamp, IServerProtocolEngine engine) + { + var elapsed = Stopwatch.GetElapsedTime(startTimestamp); + var version = engine.ProtocolVersion; + Metrics.ProtocolNegotiationDuration().Record(elapsed.TotalSeconds, + new KeyValuePair("network.protocol.version", + TurboHttpInstrumentationExtensions.FormatProtocolVersion(version))); + } + private void OnStreamCompleted(StreamCompleted msg) { var reason = _draining @@ -129,7 +146,7 @@ private void OnStreamCompleted(StreamCompleted msg) _log.Debug("Connection {0} stream completed normally", _connectionId); } - var completion = new ConnectionCompleted(_connectionId, reason); + var completion = new ConnectionCompleted(_connectionId, reason, _connectionTimestamp, _connectionActivity); Context.Parent.Tell(completion); Self.Tell(PoisonPill.Instance); } diff --git a/src/TurboHTTP/Streams/Lifecycle/Consumer.cs b/src/TurboHTTP/Streams/Lifecycle/Consumer.cs index 9073fb56c..e28208972 100644 --- a/src/TurboHTTP/Streams/Lifecycle/Consumer.cs +++ b/src/TurboHTTP/Streams/Lifecycle/Consumer.cs @@ -6,7 +6,6 @@ using Akka.Streams.Dsl; using TurboHTTP.Client; using TurboHTTP.Internal; -using TurboHTTP.Streams.Stages; using TurboHTTP.Streams.Stages.Client; namespace TurboHTTP.Streams.Lifecycle; diff --git a/src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs b/src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs index 61a17dfc6..f9010928f 100644 --- a/src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs +++ b/src/TurboHTTP/Streams/Lifecycle/ListenerActor.cs @@ -1,12 +1,15 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; using Akka; using Akka.Actor; using Akka.Event; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Routing; +using TurboHTTP.Diagnostics; using TurboHTTP.Server; -using TurboHTTP.Server.Middleware; +using static Servus.Core.Servus; namespace TurboHTTP.Streams.Lifecycle; @@ -16,8 +19,7 @@ internal sealed class ListenerActor : ReceiveActor private readonly IListenerFactory _factory; private readonly ListenerOptions _listenerOptions; private readonly TurboServerOptions _serverOptions; - private readonly TurboRequestDelegate _pipeline; - private readonly RouteTable _routeTable; + private readonly Flow _bridgeFlow; private readonly IServiceProvider _services; private readonly IMaterializer _materializer; private readonly string? _connectionLoggingCategory; @@ -25,6 +27,8 @@ internal sealed class ListenerActor : ReceiveActor private UniqueKillSwitch? _listenerKillSwitch; private int _connectionCounter; private readonly HashSet _activeConnections = []; + private readonly Dictionary _connectionMetrics = new(); + private bool _draining; public sealed record StartListening; @@ -48,8 +52,7 @@ public ListenerActor( IListenerFactory factory, ListenerOptions listenerOptions, TurboServerOptions serverOptions, - TurboRequestDelegate pipeline, - RouteTable routeTable, + Flow bridgeFlow, IServiceProvider services, IMaterializer materializer, string? connectionLoggingCategory = null) @@ -57,8 +60,7 @@ public ListenerActor( _factory = factory; _listenerOptions = listenerOptions; _serverOptions = serverOptions; - _pipeline = pipeline; - _routeTable = routeTable; + _bridgeFlow = bridgeFlow; _services = services; _materializer = materializer; _connectionLoggingCategory = connectionLoggingCategory; @@ -109,38 +111,73 @@ private void OnBindCompleted(BindCompleted msg) private void OnIncomingConnection(IncomingConnection msg) { - var limit = _serverOptions.MaxConcurrentConnections; + var limit = _serverOptions.Limits.MaxConcurrentConnections; if (limit > 0 && _activeConnections.Count >= limit) { _log.Warning("Connection rejected: limit {0} reached ({1} active)", limit, _activeConnections.Count); + if (Metrics.RejectedConnections().Enabled) + { + RecordRejectedConnection(); + } RejectConnection(msg.ConnectionFlow); return; } var connectionId = string.Concat("conn-", ++_connectionCounter); - var engine = ResolveEngineForListener(); + long timestamp = 0; + Activity? connectionActivity = null; + + if (Metrics.ActiveConnections().Enabled || Tracing.IsServerTracingActive()) + { + OnIncomingConnectionInstrumented(out timestamp, out connectionActivity); + } + var child = Context.ActorOf(ConnectionActor.Create(connectionId), connectionId); Context.Watch(child); _activeConnections.Add(child); + _connectionMetrics[child] = (timestamp, connectionActivity); child.Tell(new ConnectionActor.Materialize( msg.ConnectionFlow, engine, - _pipeline, - _routeTable, - 1, + _bridgeFlow, _services, _materializer, - _serverOptions.HandlerTimeout, - _serverOptions.HandlerGracePeriod, - _connectionLoggingCategory)); + _connectionLoggingCategory, + timestamp, + connectionActivity)); Context.Parent.Tell(new ConnectionStarted(connectionId, child)); } + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnIncomingConnectionInstrumented(out long timestamp, out Activity? connectionActivity) + { + timestamp = Stopwatch.GetTimestamp(); + var host = _listenerOptions.Host ?? "localhost"; + var port = _listenerOptions.Port; + var transport = _listenerOptions is QuicListenerOptions ? "udp" : "tcp"; + + var tags = new TagList(); + TurboServerInstrumentationExtensions.InjectConnectionTags(ref tags, host, port); + tags.Add("network.transport", transport); + Metrics.ActiveConnections().Add(1, in tags); + + connectionActivity = Tracing.StartConnectionActivity(host, port, transport); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void RecordRejectedConnection() + { + var host = _listenerOptions.Host ?? "localhost"; + Metrics.RejectedConnections().Add(1, + new KeyValuePair("server.address", host), + new KeyValuePair("server.port", _listenerOptions.Port)); + } + private void OnStopAccepting() { _log.Info("Listener stopping accept loop"); @@ -151,6 +188,12 @@ private void OnGracefulStop(GracefulStop msg) { OnStopAccepting(); + _draining = true; + if (Metrics.DrainActive().Enabled) + { + Metrics.DrainActive().Add(1); + } + foreach (var child in _activeConnections) { child.Tell(new ConnectionActor.GracefulStop(msg.Timeout)); @@ -173,6 +216,51 @@ private void OnListenerFailed(ListenerFailed msg) private void OnChildTerminated(Terminated msg) { _activeConnections.Remove(msg.ActorRef); + + if (_connectionMetrics.Remove(msg.ActorRef, out var metrics)) + { + if (Metrics.ActiveConnections().Enabled || Metrics.ConnectionDuration().Enabled || metrics.Activity is not null) + { + OnConnectionEndInstrumented(metrics.Timestamp, metrics.Activity); + } + } + + if (_draining && _activeConnections.Count == 0) + { + if (Metrics.DrainActive().Enabled) + { + Metrics.DrainActive().Add(-1); + } + _draining = false; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnConnectionEndInstrumented(long timestamp, Activity? connectionActivity) + { + var host = _listenerOptions.Host ?? "localhost"; + var port = _listenerOptions.Port; + var transport = _listenerOptions is QuicListenerOptions ? "udp" : "tcp"; + + var tags = new TagList(); + TurboServerInstrumentationExtensions.InjectConnectionTags(ref tags, host, port); + tags.Add("network.transport", transport); + + if (Metrics.ActiveConnections().Enabled) + { + Metrics.ActiveConnections().Add(-1, in tags); + } + + if (Metrics.ConnectionDuration().Enabled && timestamp > 0) + { + var elapsed = Stopwatch.GetElapsedTime(timestamp); + Metrics.ConnectionDuration().Record(elapsed.TotalSeconds, in tags); + } + + if (connectionActivity is not null) + { + Tracing.StopConnectionActivity(connectionActivity, null); + } } private void RejectConnection(Flow connectionFlow) @@ -201,13 +289,12 @@ public static Props Create( IListenerFactory factory, ListenerOptions listenerOptions, TurboServerOptions serverOptions, - TurboRequestDelegate pipeline, - RouteTable routeTable, + Flow bridgeFlow, IServiceProvider services, IMaterializer materializer, string? connectionLoggingCategory = null) => Props.Create(() => new ListenerActor( factory, listenerOptions, serverOptions, - pipeline, routeTable, services, materializer, + bridgeFlow, services, materializer, connectionLoggingCategory)); } \ No newline at end of file diff --git a/src/TurboHTTP/Streams/NegotiatingServerEngine.cs b/src/TurboHTTP/Streams/NegotiatingServerEngine.cs index ac33c922a..22aa17b38 100644 --- a/src/TurboHTTP/Streams/NegotiatingServerEngine.cs +++ b/src/TurboHTTP/Streams/NegotiatingServerEngine.cs @@ -1,6 +1,7 @@ using Akka; using Akka.Streams; using Akka.Streams.Dsl; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Server; using TurboHTTP.Streams.Stages.Server; @@ -16,7 +17,9 @@ public NegotiatingServerEngine(TurboServerOptions options) _options = options; } - public BidiFlow CreateFlow(IServiceProvider? services = null) + public Version ProtocolVersion => new(1, 1); + + public BidiFlow CreateFlow(IServiceProvider? services = null) { return BidiFlow.FromGraph(GraphDsl.Create(b => { @@ -24,8 +27,8 @@ public BidiFlow( connection.InNetwork, connection.OutRequest, diff --git a/src/TurboHTTP/Streams/Stages/Client/HttpConnectionStageLogic.cs b/src/TurboHTTP/Streams/Stages/Client/HttpConnectionStageLogic.cs index 2ca0eacb8..fd3ac3d67 100644 --- a/src/TurboHTTP/Streams/Stages/Client/HttpConnectionStageLogic.cs +++ b/src/TurboHTTP/Streams/Stages/Client/HttpConnectionStageLogic.cs @@ -17,8 +17,8 @@ internal sealed class HttpConnectionStageLogic : TimerGraphStageLogic, ICli private readonly Outlet _outNetwork; private readonly TSM _sm; - private readonly Queue _outboundQueue = new(); - private readonly Queue _responseQueue = new(); + private readonly Queue _outboundQueue = new(64); + private readonly Queue _responseQueue = new(64); private IActorRef _stageActor = ActorRefs.Nobody; public HttpConnectionStageLogic( @@ -172,14 +172,22 @@ protected override void OnTimer(object timerKey) void IClientStageOperations.OnResponse(HttpResponseMessage response) { Tracing.For("Protocol").Debug(this, "← {0}", (int)response.StatusCode); + if (IsAvailable(_outResponse)) + { + Push(_outResponse, response); + return; + } _responseQueue.Enqueue(response); - TryPushResponse(); } void IClientStageOperations.OnOutbound(ITransportOutbound item) { + if (IsAvailable(_outNetwork)) + { + Push(_outNetwork, item); + return; + } _outboundQueue.Enqueue(item); - TryPushOutbound(); } void IClientStageOperations.OnScheduleTimer(string name, TimeSpan duration) @@ -208,10 +216,67 @@ private void TryPushResponse() private void TryPushOutbound() { - if (_outboundQueue.Count > 0 && IsAvailable(_outNetwork)) + if (_outboundQueue.Count == 0 || !IsAvailable(_outNetwork)) + { + return; + } + + if (_outboundQueue.Count == 1) { Push(_outNetwork, _outboundQueue.Dequeue()); + return; + } + + if (!TryCoalesceOutbound()) + { + Push(_outNetwork, _outboundQueue.Dequeue()); + } + } + + private bool TryCoalesceOutbound() + { + var totalSize = 0; + var coalesceCount = 0; + const int maxCoalesce = 8; + + foreach (var item in _outboundQueue) + { + if (item is not TransportData { Buffer: var buf }) + { + break; + } + + totalSize += buf.Length; + coalesceCount++; + if (coalesceCount >= maxCoalesce) + { + break; + } + } + + if (coalesceCount < 2) + { + return false; } + + var merged = TransportBuffer.Rent(totalSize); + var dest = merged.FullMemory.Span; + var offset = 0; + + for (var i = 0; i < coalesceCount; i++) + { + var item = _outboundQueue.Dequeue(); + if (item is TransportData { Buffer: var buf }) + { + buf.Span.CopyTo(dest[offset..]); + offset += buf.Length; + buf.Dispose(); + } + } + + merged.Length = offset; + Push(_outNetwork, new TransportData(merged)); + return true; } private void TryPullRequest() diff --git a/src/TurboHTTP/Streams/Stages/Routing/GroupByRequestEndpointStage.cs b/src/TurboHTTP/Streams/Stages/Routing/GroupByRequestEndpointStage.cs index 9b87b7590..ce8be45ab 100644 --- a/src/TurboHTTP/Streams/Stages/Routing/GroupByRequestEndpointStage.cs +++ b/src/TurboHTTP/Streams/Stages/Routing/GroupByRequestEndpointStage.cs @@ -88,6 +88,7 @@ public SubflowState(ChannelSourceStage channelStage, RequestEndpoint key) private sealed class SubflowGroup { private readonly Dictionary _slotsById = new(); + private int _roundRobinIndex; public int Count => _slotsById.Count; public IEnumerable AllSlots => _slotsById.Values; @@ -161,6 +162,30 @@ public bool ContainsSlot(SubflowState state) return best; } + /// Returns the next alive slot in round-robin order, or null if all slots are dead. + public SubflowState? NextRoundRobin() + { + if (_slotsById.Count == 0) + { + return null; + } + + var startIndex = _roundRobinIndex; + + for (var i = 0; i < _slotsById.Count; i++) + { + var idx = (startIndex + i) % _slotsById.Count; + var slot = _slotsById.Values.ElementAt(idx); + if (!slot.IsDead) + { + _roundRobinIndex = (idx + 1) % _slotsById.Count; + return slot; + } + } + + return null; + } + /// Removes all dead slots from the lookup dictionary. Returns number removed. public int RemoveDead() { @@ -409,48 +434,34 @@ private void RouteToSlot(RequestEndpoint key, SubflowGroup group, T item) return; } - // 2. Existing slot with capacity - if (group.FindCapacitySlot() is { } capSlot) - { - Log.Debug("GroupByHostKeyStage: routed to existing slot key={0}:{1}", key.Host, key.Port); - TagAffinitySlot(item, capSlot); - capSlot.Pending.Enqueue(item); - DrainPending(key, capSlot); - return; - } - - // 3. All slots busy — try creating or use least-loaded - RouteWhenAllBusy(key, group, item); - } - - private void RouteWhenAllBusy(RequestEndpoint key, SubflowGroup group, T item) - { + // 2. Open new connection if under limit var removed = group.RemoveDead(); _totalSlotCount -= removed; - var canCreate = group.Count < _stage._maxSubstreamsPerKey(key) && + var maxPerKey = _stage._maxSubstreamsPerKey(key); + var canCreate = group.Count < maxPerKey && (_stage._maxSubstreams <= 0 || _totalSlotCount < _stage._maxSubstreams); if (canCreate) { - Log.Debug("GroupByHostKeyStage: creating additional slot for key={0}:{1}, slot={2}", key.Host, + Log.Debug("GroupByHostKeyStage: creating slot for key={0}:{1}, slot={2}", key.Host, key.Port, group.Count + 1); CreateSubstreamInGroup(key, group, item); return; } - var leastLoaded = group.FindLeastLoaded(); - if (leastLoaded != null) + // 3. Round-robin across existing connections + var slot = group.NextRoundRobin(); + if (slot != null) { - Log.Debug("GroupByHostKeyStage: all slots busy, routing to least-loaded key={0}:{1}", - key.Host, key.Port); - TagAffinitySlot(item, leastLoaded); - leastLoaded.Pending.Enqueue(item); - DrainPending(key, leastLoaded); + Log.Debug("GroupByHostKeyStage: round-robin to slot key={0}:{1}", key.Host, key.Port); + TagAffinitySlot(item, slot); + slot.Pending.Enqueue(item); + DrainPending(key, slot); return; } - // All slots dead (edge case) — create replacement + // 4. All slots dead — create replacement CreateSubstreamInGroup(key, group, item); } diff --git a/src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs b/src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs new file mode 100644 index 000000000..97aa01a3a --- /dev/null +++ b/src/TurboHTTP/Streams/Stages/Server/ApplicationBridgeStage.cs @@ -0,0 +1,457 @@ +using Akka.Actor; +using Akka.Streams; +using Akka.Streams.Stage; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using TurboHTTP.Diagnostics; +using TurboHTTP.Server.Context.Features; +using static Servus.Core.Servus; + +namespace TurboHTTP.Streams.Stages.Server; + +internal sealed class ApplicationBridgeStage : GraphStage> + where TContext : notnull +{ + private readonly IHttpApplication _application; + private readonly int _parallelism; + private readonly TimeSpan _handlerTimeout; + private readonly TimeSpan _handlerGracePeriod; + + private readonly Inlet _in = new("AppBridge.In"); + private readonly Outlet _out = new("AppBridge.Out"); + + public override FlowShape Shape { get; } + + public ApplicationBridgeStage( + IHttpApplication application, + int parallelism, + TimeSpan handlerTimeout, + TimeSpan handlerGracePeriod) + { + _application = application; + _parallelism = parallelism; + _handlerTimeout = handlerTimeout; + _handlerGracePeriod = handlerGracePeriod; + Shape = new FlowShape(_in, _out); + } + + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + + private sealed record DispatchCompleted(int Sequence, IFeatureCollection Features); + + private sealed record DispatchFailed(int Sequence, IFeatureCollection Features, Exception Error); + + private sealed record ResponseReady(int Sequence, IFeatureCollection Features, Task HandlerTask); + + private sealed record HandlerFinished(int Sequence, IFeatureCollection Features); + + private sealed record HandlerFaulted(int Sequence, IFeatureCollection Features, Exception Error); + + private sealed record HandlerTimedOut(int Sequence, IFeatureCollection Features); + + private sealed class Logic : GraphStageLogic + { + private readonly ApplicationBridgeStage _stage; + private IActorRef? _stageActor; + private bool _upstreamFinished; + private int _inFlight; + private int _sequence; + private int _nextToEmit; + private bool _downstreamReady; + private bool _unordered; + private bool _protocolDetected; + private readonly SortedDictionary _pending = []; + private readonly Dictionary _activeTimeouts = []; + private readonly Dictionary _appContexts = []; + private readonly bool _metricsEnabled; + private readonly int _backpressureThreshold; + private bool _backpressureSignaled; + + public Logic(ApplicationBridgeStage stage) : base(stage.Shape) + { + _stage = stage; + _metricsEnabled = Metrics.PipelineInFlight().Enabled + || Metrics.PipelinePending().Enabled + || Metrics.HandlerTimeouts().Enabled + || Tracing.IsServerTracingActive(); + _backpressureThreshold = (int)(stage._parallelism * 0.8); + + SetHandler(stage._in, + onPush: OnPush, + onUpstreamFinish: () => + { + _upstreamFinished = true; + if (_inFlight == 0) + { + CompleteStage(); + } + }); + + SetHandler(stage._out, + onPull: () => + { + _downstreamReady = true; + TryEmitPending(); + TryPullNext(); + }); + } + + public override void PreStart() + { + _stageActor = GetStageActor(OnMessage).Ref; + Pull(_stage._in); + } + + private void OnPush() + { + var features = Grab(_stage._in); + var seq = _sequence++; + + if (!_protocolDetected) + { + _protocolDetected = true; + var requestFeature = features.Get(); + var protocol = requestFeature?.Protocol ?? ""; + _unordered = protocol.StartsWith("HTTP/2") || protocol.StartsWith("HTTP/3"); + } + + _inFlight++; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(1); + CheckBackpressure(); + } + + try + { + DispatchAsync(features, seq); + } + catch (Exception) + { + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + } + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + } + + TryPullNext(); + } + + private void DispatchAsync(IFeatureCollection features, int seq) + { + TContext appContext; + try + { + appContext = _stage._application.CreateContext(features); + _appContexts[seq] = appContext; + } + catch (Exception) + { + _inFlight--; + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + return; + } + + var task = _stage._application.ProcessRequestAsync(appContext); + + if (task.IsCompletedSuccessfully) + { + _inFlight--; + _stage._application.DisposeContext(appContext, null); + _appContexts.Remove(seq); + CompleteResponseBody(features); + Emit(seq, features); + } + else if (task.IsFaulted) + { + _inFlight--; + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + _stage._application.DisposeContext(appContext, task.Exception); + _appContexts.Remove(seq); + CompleteResponseBody(features); + Emit(seq, features); + } + else + { + var lifetime = features.Get(); + var cts = lifetime is not null + ? CancellationTokenSource.CreateLinkedTokenSource(lifetime.RequestAborted) + : new CancellationTokenSource(); + cts.CancelAfter(_stage._handlerTimeout); + _activeTimeouts[seq] = cts; + + var bodyFeature = features.Get() as TurboHttpResponseBodyFeature; + var headersReady = bodyFeature?.WhenHeadersReady; + + Task.Delay(_stage._handlerTimeout + _stage._handlerGracePeriod, cts.Token) + .PipeTo(_stageActor!, + success: () => new HandlerTimedOut(seq, features)); + + if (headersReady is not null) + { + Task.WhenAny(headersReady, task) + .PipeTo(_stageActor!, + success: () => new ResponseReady(seq, features, task)); + } + else + { + task.PipeTo(_stageActor!, + success: () => new DispatchCompleted(seq, features), + failure: ex => new DispatchFailed(seq, features, ex)); + } + } + } + + private void OnMessage((IActorRef sender, object msg) args) + { + switch (args.msg) + { + case ResponseReady(var seq, var features, var handlerTask): + if (handlerTask.IsFaulted) + { + if (features.Get() is not TurboHttpResponseBodyFeature + { + HasStarted: true + }) + { + var responseFeature = features.Get(); + if (responseFeature is not null) + { + responseFeature.StatusCode = 500; + } + } + } + + if (handlerTask.IsCompleted) + { + CompleteResponseBody(features); + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + ResetBackpressure(); + } + DisposeCts(seq); + DisposeAppContext(seq, handlerTask.Exception); + Emit(seq, features); + } + else + { + Emit(seq, features); + handlerTask.PipeTo(_stageActor!, + success: () => new HandlerFinished(seq, features), + failure: ex => new HandlerFaulted(seq, features, ex)); + } + + break; + + case HandlerFinished(var seq, var finishedFeatures): + CompleteResponseBody(finishedFeatures); + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + ResetBackpressure(); + } + DisposeCts(seq); + DisposeAppContext(seq, null); + if (_upstreamFinished && _inFlight == 0) + { + CompleteStage(); + } + + break; + + case HandlerFaulted(var seq, var faultedFeatures, var error): + CompleteResponseBody(faultedFeatures); + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + ResetBackpressure(); + } + DisposeCts(seq); + DisposeAppContext(seq, error); + if (_upstreamFinished && _inFlight == 0) + { + CompleteStage(); + } + + break; + + case DispatchCompleted(var seq, var features): + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + ResetBackpressure(); + } + DisposeCts(seq); + DisposeAppContext(seq, null); + CompleteResponseBody(features); + Emit(seq, features); + break; + + case DispatchFailed(var seq, var features, var error): + _inFlight--; + if (_metricsEnabled) + { + Metrics.PipelineInFlight().Add(-1); + ResetBackpressure(); + } + DisposeCts(seq); + DisposeAppContext(seq, error); + var respFeature = features.Get(); + if (respFeature is not null) + { + respFeature.StatusCode = 500; + } + CompleteResponseBody(features); + Emit(seq, features); + break; + + case HandlerTimedOut(var seq, var features): + if (_activeTimeouts.TryGetValue(seq, out var cts)) + { + cts.Dispose(); + _activeTimeouts.Remove(seq); + var respFeatureTimeout = features.Get(); + if (respFeatureTimeout is not null && respFeatureTimeout.StatusCode == 200) + { + respFeatureTimeout.StatusCode = 503; + CompleteResponseBody(features); + _inFlight--; + if (_metricsEnabled) + { + Metrics.HandlerTimeouts().Add(1); + Metrics.PipelineInFlight().Add(-1); + } + DisposeAppContext(seq, null); + Emit(seq, features); + } + } + + break; + } + + if (_upstreamFinished && _inFlight == 0 && _pending.Count == 0) + { + CompleteStage(); + } + } + + private void DisposeAppContext(int seq, Exception? exception) + { + if (_appContexts.TryGetValue(seq, out var appCtx)) + { + _stage._application.DisposeContext(appCtx, exception); + _appContexts.Remove(seq); + } + } + + private void DisposeCts(int seq) + { + if (_activeTimeouts.TryGetValue(seq, out var cts)) + { + cts.Dispose(); + _activeTimeouts.Remove(seq); + } + } + + private void TryPullNext() + { + if (_inFlight < _stage._parallelism && !HasBeenPulled(_stage._in)) + { + Pull(_stage._in); + } + } + + private void Emit(int seq, IFeatureCollection features) + { + _pending[seq] = features; + if (_metricsEnabled) + { + Metrics.PipelinePending().Add(1); + } + TryEmitPending(); + } + + private void TryEmitPending() + { + if (_unordered) + { + if (_downstreamReady && _pending.Count > 0) + { + var seq = _pending.Keys.First(); + EmitOne(seq); + } + } + else + { + while (_downstreamReady && _pending.Count > 0 && _pending.Keys.First() == _nextToEmit) + { + EmitOne(_nextToEmit); + _nextToEmit++; + } + } + } + + private void EmitOne(int seq) + { + _downstreamReady = false; + Push(_stage._out, _pending[seq]); + _pending.Remove(seq); + if (_metricsEnabled) + { + Metrics.PipelinePending().Add(-1); + } + } + + private static void CompleteResponseBody(IFeatureCollection features) + { + var bodyFeature = features.Get() as TurboHttpResponseBodyFeature; + bodyFeature?.Complete(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void CheckBackpressure() + { + if (_inFlight >= _backpressureThreshold && !_backpressureSignaled) + { + _backpressureSignaled = true; + if (Activity.Current is { } connectionActivity) + { + Tracing.AddBackpressureEvent(connectionActivity, _inFlight, _stage._parallelism); + } + } + } + + private void ResetBackpressure() + { + if (_backpressureSignaled && _inFlight < _backpressureThreshold) + { + _backpressureSignaled = false; + } + } + } +} diff --git a/src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs index 206ce7e95..010f068ce 100644 --- a/src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Server/Http10ServerConnectionStage.cs @@ -1,5 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http10.Server; using TurboHTTP.Server; @@ -9,8 +10,8 @@ namespace TurboHTTP.Streams.Stages.Server; internal sealed class Http10ServerConnectionStage : GraphStage { private readonly Inlet _inNetwork = new("Http10Connection.In.Network"); - private readonly Outlet _outRequest = new("Http10Connection.Out.Request"); - private readonly Inlet _inResponse = new("Http10Connection.In.Response"); + private readonly Outlet _outRequest = new("Http10Connection.Out.Request"); + private readonly Inlet _inResponse = new("Http10Connection.In.Response"); private readonly Outlet _outNetwork = new("Http10Connection.Out.Network"); private readonly TurboServerOptions _options; private readonly IServiceProvider? _services; diff --git a/src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs index 0ccb2eed6..326472450 100644 --- a/src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Server/Http11ServerConnectionStage.cs @@ -1,5 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http11.Server; using TurboHTTP.Server; @@ -9,8 +10,8 @@ namespace TurboHTTP.Streams.Stages.Server; internal sealed class Http11ServerConnectionStage : GraphStage { private readonly Inlet _inNetwork = new("Http11Connection.In.Network"); - private readonly Outlet _outRequest = new("Http11Connection.Out.Request"); - private readonly Inlet _inResponse = new("Http11Connection.In.Response"); + private readonly Outlet _outRequest = new("Http11Connection.Out.Request"); + private readonly Inlet _inResponse = new("Http11Connection.In.Response"); private readonly Outlet _outNetwork = new("Http11Connection.Out.Network"); private readonly TurboServerOptions _options; private readonly IServiceProvider? _services; diff --git a/src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs index 9f1f67c8b..c2660d00a 100644 --- a/src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Server/Http20ServerConnectionStage.cs @@ -1,5 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http2.Server; using TurboHTTP.Server; @@ -9,8 +10,8 @@ namespace TurboHTTP.Streams.Stages.Server; internal sealed class Http20ServerConnectionStage : GraphStage { private readonly Inlet _inNetwork = new("Http20Connection.In.Network"); - private readonly Outlet _outRequest = new("Http20Connection.Out.Request"); - private readonly Inlet _inResponse = new("Http20Connection.In.Response"); + private readonly Outlet _outRequest = new("Http20Connection.Out.Request"); + private readonly Inlet _inResponse = new("Http20Connection.In.Response"); private readonly Outlet _outNetwork = new("Http20Connection.Out.Network"); private readonly TurboServerOptions _options; private readonly IServiceProvider? _services; diff --git a/src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs index ecc5b6f83..245b4b1fc 100644 --- a/src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Server/Http30ServerConnectionStage.cs @@ -1,5 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol.Syntax.Http3.Server; using TurboHTTP.Server; @@ -9,8 +10,8 @@ namespace TurboHTTP.Streams.Stages.Server; internal sealed class Http30ServerConnectionStage : GraphStage { private readonly Inlet _inNetwork = new("Http30Connection.In.Network"); - private readonly Outlet _outRequest = new("Http30Connection.Out.Request"); - private readonly Inlet _inResponse = new("Http30Connection.In.Response"); + private readonly Outlet _outRequest = new("Http30Connection.Out.Request"); + private readonly Inlet _inResponse = new("Http30Connection.In.Response"); private readonly Outlet _outNetwork = new("Http30Connection.Out.Network"); private readonly TurboServerOptions _options; private readonly IServiceProvider? _services; diff --git a/src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs b/src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs index 9aecfb1f3..d024b3536 100644 --- a/src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs +++ b/src/TurboHTTP/Streams/Stages/Server/HttpConnectionServerStageLogic.cs @@ -1,11 +1,15 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; using Akka.Actor; using Akka.Event; using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; +using TurboHTTP.Diagnostics; using TurboHTTP.Protocol; using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; using static Servus.Core.Servus; namespace TurboHTTP.Streams.Stages.Server; @@ -14,17 +18,18 @@ internal sealed class HttpConnectionServerStageLogic : TimerGraphStageLogic where TSM : IServerStateMachine { private readonly Inlet _inNetwork; - private readonly Outlet _outRequest; - private readonly Inlet _inResponse; + private readonly Outlet _outRequest; + private readonly Inlet _inResponse; private readonly Outlet _outNetwork; private readonly TSM _sm; - private readonly Queue _requestQueue = new(); + private readonly Queue _requestQueue = new(); private readonly Queue _outboundQueue = new(); private IActorRef _stageActor = ActorRefs.Nobody; private readonly IServiceProvider? _services; - private TurboConnectionInfo? _connectionInfo; + private TurboHttpConnectionFeature? _connectionFeature; private TlsHandshakeFeature? _tlsHandshakeFeature; + private readonly bool _metricsEnabled; public HttpConnectionServerStageLogic( GraphStage stage, @@ -39,6 +44,9 @@ public HttpConnectionServerStageLogic( _services = services; _sm = smFactory(this); + _metricsEnabled = Metrics.ServerActiveRequests().Enabled + || Metrics.ServerRequestDuration().Enabled + || Tracing.IsServerTracingActive(); SetHandler(_inNetwork, onPush: OnNetworkPush, @@ -84,15 +92,24 @@ public HttpConnectionServerStageLogic( if (_sm.ShouldComplete) { + if (_metricsEnabled) + { + OnResponseInstrumented(response); + } CompleteStage(); return; } - var bodyFeature = response.TurboResponse.HttpContext.Features.Get(); + if (_metricsEnabled) + { + OnResponseInstrumented(response); + } + + var bodyFeature = response.Get(); var hasBody = bodyFeature is not null; if (!hasBody) { - ServerContextFactory.Return(response); + FeatureCollectionFactory.Return(response); } TryPullResponse(); @@ -118,7 +135,11 @@ public override void PreStart() Pull(_inNetwork); } - private void OnStageActorMessage((IActorRef sender, object message) args) => _sm.OnBodyMessage(args.message); + private void OnStageActorMessage((IActorRef sender, object message) args) + { + _sm.OnBodyMessage(args.message); + TryPushOutbound(); + } private void OnNetworkPush() { @@ -129,17 +150,17 @@ private void OnNetworkPush() var info = connected.Info; if (info.Remote is System.Net.IPEndPoint remoteEp) { - _connectionInfo = new TurboConnectionInfo( - Guid.NewGuid().ToString("N"), - remoteEp.Address, remoteEp.Port, - (info.Local as System.Net.IPEndPoint)?.Address, - (info.Local as System.Net.IPEndPoint)?.Port ?? 0); + var connectionFeature = new TurboHttpConnectionFeature + { + ConnectionId = Guid.NewGuid().ToString("N"), + RemoteIpAddress = remoteEp.Address, + RemotePort = remoteEp.Port, + LocalIpAddress = (info.Local as System.Net.IPEndPoint)?.Address, + LocalPort = (info.Local as System.Net.IPEndPoint)?.Port ?? 0, + }; if (info.Security is { } security) { - _connectionInfo.SetSecurityInfo(security); - _connectionInfo.SetNegotiatedProtocol(security.ApplicationProtocol); - _tlsHandshakeFeature = new TlsHandshakeFeature { Protocol = security.Protocol, @@ -147,13 +168,9 @@ private void OnNetworkPush() HostName = security.HostName, NegotiatedApplicationProtocol = security.ApplicationProtocol, }; - - if (security.SslStream is not null) - { - _connectionInfo.SetClientCertificateFromHandshake(security.SslStream); - _connectionInfo.SetTlsState(security.SslStream, security.AllowDelayedNegotiation); - } } + + _connectionFeature = connectionFeature; } } @@ -198,7 +215,7 @@ protected override void OnTimer(object timerKey) } } - void IServerStageOperations.OnRequest(TurboHttpContext context) + void IServerStageOperations.OnRequest(IFeatureCollection features) { if (_requestQueue.Count >= _sm.MaxQueuedRequests) { @@ -207,11 +224,79 @@ void IServerStageOperations.OnRequest(TurboHttpContext context) return; } - context.Materializer = Materializer; - _requestQueue.Enqueue(context); + if (_metricsEnabled) + { + OnRequestInstrumented(features); + } + + _requestQueue.Enqueue(features); TryPushRequest(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnRequestInstrumented(IFeatureCollection features) + { + var requestFeature = features.Get(); + if (requestFeature is null) + { + return; + } + + var method = requestFeature.Method; + var path = requestFeature.Path; + var scheme = requestFeature.Scheme ?? "http"; + + if (Metrics.ServerActiveRequests().Enabled) + { + Metrics.ServerActiveRequests().Add(1, + new KeyValuePair("url.scheme", scheme), + new KeyValuePair("http.request.method", + TurboHttpInstrumentationExtensions.NormalizeMethod(method))); + } + + if (features is TurboFeatureCollection turbo) + { + turbo.RequestTimestamp = Stopwatch.GetTimestamp(); + turbo.RequestActivity = Tracing.StartRequestActivity(method, path, scheme); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnResponseInstrumented(IFeatureCollection features) + { + var responseFeature = features.Get(); + var requestFeature = features.Get(); + var statusCode = responseFeature?.StatusCode ?? 0; + + if (requestFeature is not null && Metrics.ServerActiveRequests().Enabled) + { + var scheme = requestFeature.Scheme ?? "http"; + Metrics.ServerActiveRequests().Add(-1, + new KeyValuePair("url.scheme", scheme), + new KeyValuePair("http.request.method", + TurboHttpInstrumentationExtensions.NormalizeMethod(requestFeature.Method))); + } + + if (features is TurboFeatureCollection turbo) + { + if (turbo.RequestActivity is { } activity) + { + Tracing.SetServerResponse(activity, statusCode); + activity.Stop(); + } + + if (turbo.RequestTimestamp > 0 && Metrics.ServerRequestDuration().Enabled && requestFeature is not null) + { + var elapsed = Stopwatch.GetElapsedTime(turbo.RequestTimestamp); + Metrics.ServerRequestDuration().Record(elapsed.TotalSeconds, + new KeyValuePair("http.request.method", + TurboHttpInstrumentationExtensions.NormalizeMethod(requestFeature.Method)), + new KeyValuePair("http.response.status_code", statusCode), + new KeyValuePair("url.scheme", requestFeature.Scheme ?? "http")); + } + } + } + void IServerStageOperations.OnOutbound(ITransportOutbound item) { _outboundQueue.Enqueue(item); @@ -232,7 +317,7 @@ void IServerStageOperations.OnCancelTimer(string name) IServiceProvider? IServerStageOperations.Services => _services; - TurboConnectionInfo? IServerStageOperations.ConnectionInfo => _connectionInfo; + TurboHttpConnectionFeature? IServerStageOperations.ConnectionFeature => _connectionFeature; TlsHandshakeFeature? IServerStageOperations.TlsHandshakeFeature => _tlsHandshakeFeature; diff --git a/src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs b/src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs index 1a2f8a0dc..b0f429f87 100644 --- a/src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs +++ b/src/TurboHTTP/Streams/Stages/Server/IServerStageOperations.cs @@ -1,15 +1,15 @@ using Akka.Actor; using Akka.Event; using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Context.Features; -using TurboHTTP.Server; +using TurboHTTP.Server.Context.Features; namespace TurboHTTP.Streams.Stages.Server; internal interface IServerStageOperations { - void OnRequest(TurboHttpContext context); + void OnRequest(IFeatureCollection features); void OnOutbound(ITransportOutbound item); void OnScheduleTimer(string name, TimeSpan delay); void OnCancelTimer(string name); @@ -17,6 +17,6 @@ internal interface IServerStageOperations IActorRef StageActor { get; } IMaterializer Materializer { get; } IServiceProvider? Services => null; - TurboConnectionInfo? ConnectionInfo => null; + TurboHttpConnectionFeature? ConnectionFeature => null; TlsHandshakeFeature? TlsHandshakeFeature => null; } \ No newline at end of file diff --git a/src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs b/src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs index ad5096913..3e5955a98 100644 --- a/src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs +++ b/src/TurboHTTP/Streams/Stages/Server/ProtocolNegotiatorConnectionStage.cs @@ -1,5 +1,6 @@ using Akka.Streams; using Akka.Streams.Stage; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; using TurboHTTP.Protocol; using TurboHTTP.Server; @@ -9,8 +10,8 @@ namespace TurboHTTP.Streams.Stages.Server; internal sealed class ProtocolNegotiatorConnectionStage : GraphStage { private readonly Inlet _inNetwork = new("NegotiatorConnection.In.Network"); - private readonly Outlet _outRequest = new("NegotiatorConnection.Out.Request"); - private readonly Inlet _inResponse = new("NegotiatorConnection.In.Response"); + private readonly Outlet _outRequest = new("NegotiatorConnection.Out.Request"); + private readonly Inlet _inResponse = new("NegotiatorConnection.In.Response"); private readonly Outlet _outNetwork = new("NegotiatorConnection.Out.Network"); private readonly TurboServerOptions _options; private readonly IServiceProvider? _services; diff --git a/src/TurboHTTP/Streams/Stages/Server/RoutingStage.cs b/src/TurboHTTP/Streams/Stages/Server/RoutingStage.cs deleted file mode 100644 index c81bb9f9a..000000000 --- a/src/TurboHTTP/Streams/Stages/Server/RoutingStage.cs +++ /dev/null @@ -1,317 +0,0 @@ -using Akka.Actor; -using Akka.Streams; -using Akka.Streams.Stage; -using TurboHTTP.Context.Features; -using TurboHTTP.Routing; -using TurboHTTP.Server; - -namespace TurboHTTP.Streams.Stages.Server; - -internal sealed class RoutingStage : GraphStage> -{ - private readonly RouteTable _routeTable; - private readonly TurboRequestDelegate _pipeline; - private readonly int _parallelism; - private readonly TimeSpan _handlerTimeout; - private readonly TimeSpan _handlerGracePeriod; - - private readonly Inlet _in = new("Routing.In"); - private readonly Outlet _out = new("Routing.Out"); - - public override FlowShape Shape { get; } - - public RoutingStage(RouteTable routeTable, TurboRequestDelegate pipeline, int parallelism, TimeSpan handlerTimeout, - TimeSpan handlerGracePeriod) - { - _routeTable = routeTable; - _pipeline = pipeline; - _parallelism = parallelism; - _handlerTimeout = handlerTimeout; - _handlerGracePeriod = handlerGracePeriod; - Shape = new FlowShape(_in, _out); - } - - protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); - - private sealed record DispatchCompleted(int Sequence, TurboHttpContext Context); - - private sealed record DispatchFailed(int Sequence, TurboHttpContext Context, Exception Error); - - private sealed record ResponseReady(int Sequence, TurboHttpContext Context, Task HandlerTask); - - private sealed record HandlerFinished(int Sequence, TurboHttpContext Context); - - private sealed record HandlerFaulted(int Sequence, TurboHttpContext Context, Exception Error); - - private sealed record HandlerTimedOut(int Sequence, TurboHttpContext Context); - - private sealed class Logic : GraphStageLogic - { - private readonly RoutingStage _stage; - private IActorRef? _stageActor; - private bool _upstreamFinished; - private int _inFlight; - private int _sequence; - private int _nextToEmit; - private bool _downstreamReady; - private readonly SortedDictionary _pending = []; - private readonly Dictionary _activeTimeouts = []; - - public Logic(RoutingStage stage) : base(stage.Shape) - { - _stage = stage; - - SetHandler(stage._in, - onPush: OnPush, - onUpstreamFinish: () => - { - _upstreamFinished = true; - if (_inFlight == 0) - { - CompleteStage(); - } - }); - - SetHandler(stage._out, - onPull: () => - { - _downstreamReady = true; - TryEmitPending(); - TryPullNext(); - }); - } - - public override void PreStart() - { - _stageActor = GetStageActor(OnMessage).Ref; - Pull(_stage._in); - } - - private void OnPush() - { - var ctx = Grab(_stage._in); - var seq = _sequence++; - var path = ctx.Request.Path.Value ?? "/"; - - var match = _stage._routeTable.Match(ctx.Request.Method, path); - if (match is not { IsMatch: true, Dispatcher: not null }) - { - ctx.Response.StatusCode = 404; - CompleteResponseBody(ctx); - Emit(seq, ctx); - return; - } - - foreach (var kv in match.RouteValues) - { - ctx.Request.RouteValues[kv.Key] = kv.Value; - } - - if (match.Metadata is not null) - { - ctx.EndpointMetadata = match.Metadata; - } - - _inFlight++; - - try - { - DispatchAsync(ctx, seq, match); - } - catch (Exception) - { - _inFlight--; - ctx.Response.StatusCode = 500; - CompleteResponseBody(ctx); - Emit(seq, ctx); - } - - TryPullNext(); - } - - private void DispatchAsync(TurboHttpContext ctx, int seq, RouteMatchResult match) - { - var task = DispatchAsyncInternal(ctx, seq, match); - - if (task.IsCompletedSuccessfully) - { - _inFlight--; - CompleteResponseBody(ctx); - Emit(seq, ctx); - } - else if (task.IsFaulted) - { - _inFlight--; - ctx.Response.StatusCode = 500; - CompleteResponseBody(ctx); - Emit(seq, ctx); - } - else - { - var cts = CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted); - cts.CancelAfter(_stage._handlerTimeout); - _activeTimeouts[seq] = cts; - ctx.RequestAborted = cts.Token; - - var bodyFeature = ctx.Features.Get() as TurboHttpResponseBodyFeature; - var headersReady = bodyFeature?.WhenHeadersReady; - - Task.Delay(_stage._handlerTimeout + _stage._handlerGracePeriod, cts.Token) - .PipeTo(_stageActor!, - success: () => new HandlerTimedOut(seq, ctx)); - - if (headersReady is not null) - { - Task.WhenAny(headersReady, task) - .PipeTo(_stageActor!, - success: () => new ResponseReady(seq, ctx, task)); - } - else - { - task.PipeTo(_stageActor!, - success: () => new DispatchCompleted(seq, ctx), - failure: ex => new DispatchFailed(seq, ctx, ex)); - } - } - } - - private async Task DispatchAsyncInternal(TurboHttpContext ctx, int seq, RouteMatchResult match) - { - await _stage._pipeline(ctx); - await match.Dispatcher!.DispatchAsync(ctx, ctx.RequestAborted); - } - - private void OnMessage((IActorRef sender, object msg) args) - { - switch (args.msg) - { - case ResponseReady(var seq, var ctx, var handlerTask): - if (handlerTask.IsFaulted) - { - if (ctx.Features.Get() is not TurboHttpResponseBodyFeature - { - HasStarted: true - }) - { - ctx.Response.StatusCode = 500; - } - } - - if (handlerTask.IsCompleted) - { - CompleteResponseBody(ctx); - _inFlight--; - DisposeCts(seq); - Emit(seq, ctx); - } - else - { - Emit(seq, ctx); - handlerTask.PipeTo(_stageActor!, - success: () => new HandlerFinished(seq, ctx), - failure: ex => new HandlerFaulted(seq, ctx, ex)); - } - - break; - - case HandlerFinished(var seq, var finishedCtx): - CompleteResponseBody(finishedCtx); - _inFlight--; - DisposeCts(seq); - if (_upstreamFinished && _inFlight == 0) - { - CompleteStage(); - } - - break; - - case HandlerFaulted(var seq, var faultedCtx, _): - CompleteResponseBody(faultedCtx); - _inFlight--; - DisposeCts(seq); - if (_upstreamFinished && _inFlight == 0) - { - CompleteStage(); - } - - break; - - case DispatchCompleted(var seq, var ctx): - _inFlight--; - DisposeCts(seq); - CompleteResponseBody(ctx); - Emit(seq, ctx); - break; - - case DispatchFailed(var seq, var ctx, _): - _inFlight--; - DisposeCts(seq); - ctx.Response.StatusCode = 500; - CompleteResponseBody(ctx); - Emit(seq, ctx); - break; - - case HandlerTimedOut(var seq, var ctx): - if (_activeTimeouts.TryGetValue(seq, out var cts)) - { - cts.Dispose(); - _activeTimeouts.Remove(seq); - if (!ctx.Response.HasStarted) - { - ctx.Response.StatusCode = 503; - CompleteResponseBody(ctx); - _inFlight--; - Emit(seq, ctx); - } - } - - break; - } - - if (_upstreamFinished && _inFlight == 0 && _pending.Count == 0) - { - CompleteStage(); - } - } - - private void DisposeCts(int seq) - { - if (_activeTimeouts.TryGetValue(seq, out var cts)) - { - cts.Dispose(); - _activeTimeouts.Remove(seq); - } - } - - private void TryPullNext() - { - if (_inFlight < _stage._parallelism && !HasBeenPulled(_stage._in)) - { - Pull(_stage._in); - } - } - - private void Emit(int seq, TurboHttpContext ctx) - { - _pending[seq] = ctx; - TryEmitPending(); - } - - private void TryEmitPending() - { - while (_downstreamReady && _pending.Count > 0 && _pending.Keys.First() == _nextToEmit) - { - _downstreamReady = false; - Push(_stage._out, _pending[_nextToEmit]); - _pending.Remove(_nextToEmit); - _nextToEmit++; - } - } - - private static void CompleteResponseBody(TurboHttpContext ctx) - { - var bodyFeature = ctx.Features.Get() as TurboHttpResponseBodyFeature; - bodyFeature?.Complete(); - } - } -} \ No newline at end of file diff --git a/src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs b/src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs index d65afd237..024f5a89b 100644 --- a/src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs +++ b/src/TurboHTTP/Streams/Stages/Server/ServerConnectionShape.cs @@ -1,21 +1,21 @@ using System.Collections.Immutable; using Akka.Streams; +using Microsoft.AspNetCore.Http.Features; using Servus.Akka.Transport; -using TurboHTTP.Server; namespace TurboHTTP.Streams.Stages.Server; internal sealed class ServerConnectionShape : Shape { public Inlet InNetwork { get; } - public Outlet OutRequest { get; } - public Inlet InResponse { get; } + public Outlet OutRequest { get; } + public Inlet InResponse { get; } public Outlet OutNetwork { get; } public ServerConnectionShape( Inlet inNetwork, - Outlet outResponse, - Inlet inRequest, + Outlet outResponse, + Inlet inRequest, Outlet outNetwork) { InNetwork = inNetwork; @@ -32,8 +32,8 @@ public override Shape DeepCopy() { return new ServerConnectionShape( (Inlet)InNetwork.CarbonCopy(), - (Outlet)OutRequest.CarbonCopy(), - (Inlet)InResponse.CarbonCopy(), + (Outlet)OutRequest.CarbonCopy(), + (Inlet)InResponse.CarbonCopy(), (Outlet)OutNetwork.CarbonCopy()); } @@ -41,8 +41,8 @@ public override Shape CopyFromPorts(ImmutableArray inlets, ImmutableArray { return new ServerConnectionShape( (Inlet)inlets[0], - (Outlet)outlets[0], - (Inlet)inlets[1], + (Outlet)outlets[0], + (Inlet)inlets[1], (Outlet)outlets[1]); } } diff --git a/src/TurboHTTP/TurboHTTP.csproj b/src/TurboHTTP/TurboHTTP.csproj index b299d9997..3c15fc169 100644 --- a/src/TurboHTTP/TurboHTTP.csproj +++ b/src/TurboHTTP/TurboHTTP.csproj @@ -1,7 +1,7 @@  - leberkas-org + leberkas-org TurboHTTP High-performance HTTP client and server for .NET built on Akka.Streams. Supports HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) with backpressure, connection pooling, retries, cookies, caching, and an actor-based entity gateway. http;http2;http3;quic;akka;akka-streams;reactive;backpressure;httpclient;httpserver @@ -22,11 +22,7 @@ embedded true - - - - - + @@ -38,6 +34,7 @@ + @@ -48,7 +45,6 @@ - diff --git a/src/TurboHTTP/packages.lock.json b/src/TurboHTTP/packages.lock.json index b9c1ff0e2..2c14317ca 100644 --- a/src/TurboHTTP/packages.lock.json +++ b/src/TurboHTTP/packages.lock.json @@ -10,6 +10,10 @@ "dependencies": { "Akka.DependencyInjection": "1.5.68", "Akka.Streams": "1.5.68", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.HealthChecks": "9.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", "OpenTelemetry": "1.10.0" } }, @@ -24,6 +28,15 @@ "Reactive.Streams": "1.0.4" } }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Direct", + "requested": "[2.3.10, )", + "resolved": "2.3.10", + "contentHash": "FD6mE5v3qB/sEcnLNni1oHsATuLHIFzsKHCulUZS7iaJez8+TkD432L89DQtU9PfjCkaPO0JOQjB4tS4L8oZXQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.9" + } + }, "Microsoft.IO.RecyclableMemoryStream": { "type": "Direct", "requested": "[3.0.1, )", @@ -36,6 +49,7 @@ "contentHash": "N/bBk9MnlIPfhqxPQVggoCdW2n0EUZAbvyxDHOk5zg5acTOffFqTSIpCcfH4e257LlyUPrDuE2EaikbUmG/01w==", "dependencies": { "Akka.Analyzers": "0.3.3", + "Microsoft.Extensions.ObjectPool": "6.0.36", "Newtonsoft.Json": "13.0.1", "System.Configuration.ConfigurationManager": "6.0.1" } @@ -50,7 +64,8 @@ "resolved": "1.5.68", "contentHash": "dk1q/8wF0cMdbUegU64DTJK8lW9d+/QWDFZFLDJ82YLb0Ewg8o3gRHWSMAUt+4pFduh62Zuyxhvg5qhOeaOVgA==", "dependencies": { - "Akka": "1.5.68" + "Akka": "1.5.68", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" } }, "Google.Protobuf": { @@ -58,6 +73,162 @@ "resolved": "3.26.1", "contentHash": "CHZX8zXqhF/fdUtd+AYzew8T2HFkAoe5c7lbGxZY/qryAlQXckDvM5BfOJjXlMS7kyICqQTMszj4w1bX5uBJ/w==" }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.9", + "contentHash": "nDWDF0YBgElg6f0omqJ8DupmITQg5p4lklBxFpVR83tQQhOG2tw9Fa5ul8b+KywsYBsyfn9DschUmfzOV6RvGw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "cxsK9/Dx7Ka9sfiA1nY8XlSzIaWff5FNRw0+ls8yR+aGzmnah5JGKsTHgQrehjMwGAVud/pjiUZ9MWizfUSkTQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "9.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "H1IbHm/MnUgEV0N07WrkPBIIoX7isP6IPaqUdZ3CwbMcUVDGIu+okamW28kyDRfIiZqbTbyHuNIkr4ZSHPyvDw==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.15", + "contentHash": "yzWilnNU/MvHINapPhY6iFAeApZnhToXbEBplORucn01hFc1F6ZaKt0V9dHYpUMun8WR9cSnq1ky35FWREVZbA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.15" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "9.0.15", + "contentHash": "fYrCuUAhXdeIcwPtyThTmEJ1KyUgTqwynzBCQ4n/SnpyC8/DW8GZCxGrnj9k7r0zcJy7GGaPbnZqrVRN52yZuA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.15", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.15", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.15", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.15", + "Microsoft.Extensions.Logging.Abstractions": "9.0.15" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "j8zcwhS6bYB6FEfaY3nYSgHdpiL2T+/V3xjpHtslVAegyI1JUbB9yAt/BFdvZdsNbY0Udm4xFtvfT/hUwcOOOg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "6.0.36", + "contentHash": "WHwL2Src2pAiLGEmzq6htW+jeKIkrifT3Q6nZROY6EG8EbSGli//XpAh9WpITRsTda3VVUBegvc2QdBWyNn+zg==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "tL9cSl3maS5FPzp/3MtlZI21ExWhni0nnUCF8HY4npTsINw45n9SNDbkKXBMtFyUFGSsQep25fHIDN4f/Vp3AQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==" + }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", "resolved": "6.0.0", @@ -115,7 +286,7 @@ "type": "Project", "dependencies": { "Akka.Hosting": "[1.5.68, )", - "Servus.Core": "[0.33.10, )" + "Servus.Core": "[0.33.11, )" } }, "OpenTelemetry": { @@ -124,6 +295,8 @@ "resolved": "1.15.3", "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==", "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3" } }, @@ -139,14 +312,19 @@ "resolved": "1.15.3", "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==", "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "OpenTelemetry.Api": "1.15.3" } }, "Servus.Core": { "type": "CentralTransitive", - "requested": "[0.33.10, )", - "resolved": "0.33.10", - "contentHash": "31KtCHbrqw6IXPaMqdVPUqQmZwDHtDL9SPWgqYUnmk6hoSi8FgoUOjv6GiMoUGDlJHiCtaNbW+UADjpCl8tv0A==" + "requested": "[0.33.11, )", + "resolved": "0.33.11", + "contentHash": "j3MSNKNN9T53Uzkhktgwqi0cnITq/eX6CU/cwy5wN/UVCUwf2Q7al0u6ofGrQoDoqtCObRgvanU02PjYwQWCGw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.15", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.15" + } } } }