using Microsoft.Extensions.Options; using StellaOps.Router.Common.Abstractions; using StellaOps.Router.Common.Models; using StellaOps.Router.Gateway.Configuration; namespace StellaOps.Router.Gateway.Middleware; /// /// Makes routing decisions for resolved endpoints. /// public sealed class RoutingDecisionMiddleware { private readonly RequestDelegate _next; /// /// Initializes a new instance of the class. /// public RoutingDecisionMiddleware(RequestDelegate next) { _next = next; } /// /// Invokes the middleware. /// public async Task Invoke( HttpContext context, IRoutingPlugin routingPlugin, IGlobalRoutingState routingState, IOptions gatewayConfig, IOptions routingOptions) { var endpoint = context.Items[RouterHttpContextKeys.EndpointDescriptor] as EndpointDescriptor; if (endpoint is null) { await RouterErrorWriter.WriteAsync( context, statusCode: StatusCodes.Status500InternalServerError, error: "Endpoint descriptor missing", cancellationToken: context.RequestAborted); return; } // Build routing context var availableConnections = routingState.GetConnectionsFor( endpoint.ServiceName, endpoint.Version, endpoint.Method, endpoint.Path); var headers = context.Request.Headers .ToDictionary(h => h.Key, h => h.Value.ToString()); var routingContext = new RoutingContext { Method = context.Request.Method, Path = context.Request.Path.ToString(), Headers = headers, Endpoint = endpoint, AvailableConnections = availableConnections, GatewayRegion = gatewayConfig.Value.Region, RequestedVersion = ExtractVersionFromRequest(context, routingOptions.Value), RouteDefaultTimeout = context.Items.TryGetValue(RouterHttpContextKeys.RouteDefaultTimeout, out var routeTimeoutObj) && routeTimeoutObj is TimeSpan routeTimeout ? routeTimeout : null, CancellationToken = context.RequestAborted }; var decision = await routingPlugin.ChooseInstanceAsync( routingContext, context.RequestAborted); if (decision is null) { await RouterErrorWriter.WriteAsync( context, statusCode: StatusCodes.Status503ServiceUnavailable, error: "No instances available", service: endpoint.ServiceName, version: endpoint.Version, cancellationToken: context.RequestAborted); return; } context.Items[RouterHttpContextKeys.RoutingDecision] = decision; await _next(context); } private static string? ExtractVersionFromRequest(HttpContext context, RoutingOptions options) { // Check for version in Accept header: Accept: application/vnd.stellaops.v1+json var acceptHeader = context.Request.Headers.Accept.FirstOrDefault(); if (!string.IsNullOrEmpty(acceptHeader)) { var versionMatch = System.Text.RegularExpressions.Regex.Match( acceptHeader, @"application/vnd\.stellaops\.v(\d+(?:\.\d+)*)\+json"); if (versionMatch.Success) { return versionMatch.Groups[1].Value; } } // Check for X-Api-Version header var versionHeader = context.Request.Headers["X-Api-Version"].FirstOrDefault(); if (!string.IsNullOrEmpty(versionHeader)) { return versionHeader; } // Fall back to default version from options return options.DefaultVersion; } }