Files
git.stella-ops.org/src/Router/__Libraries/StellaOps.Router.Gateway/Middleware/RoutingDecisionMiddleware.cs

116 lines
4.0 KiB
C#

using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.Configuration;
namespace StellaOps.Router.Gateway.Middleware;
/// <summary>
/// Makes routing decisions for resolved endpoints.
/// </summary>
public sealed class RoutingDecisionMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// Initializes a new instance of the <see cref="RoutingDecisionMiddleware"/> class.
/// </summary>
public RoutingDecisionMiddleware(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// Invokes the middleware.
/// </summary>
public async Task Invoke(
HttpContext context,
IRoutingPlugin routingPlugin,
IGlobalRoutingState routingState,
IOptions<RouterNodeConfig> gatewayConfig,
IOptions<RoutingOptions> 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;
}
}