Fix router frontdoor readiness and route contracts
This commit is contained in:
@@ -20,7 +20,11 @@ public sealed class HealthCheckMiddleware
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, GatewayServiceStatus status, GatewayMetrics metrics)
|
||||
public async Task InvokeAsync(
|
||||
HttpContext context,
|
||||
GatewayServiceStatus status,
|
||||
GatewayMetrics metrics,
|
||||
GatewayReadinessEvaluator readinessEvaluator)
|
||||
{
|
||||
if (GatewayRoutes.IsMetricsPath(context.Request.Path))
|
||||
{
|
||||
@@ -37,28 +41,34 @@ public sealed class HealthCheckMiddleware
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
if (path.Equals("/health/live", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "live", status);
|
||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "live", status, readinessEvaluator.Evaluate(status));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.Equals("/health/ready", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var readyStatus = status.IsReady ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||
await WriteHealthAsync(context, readyStatus, "ready", status);
|
||||
var readiness = readinessEvaluator.Evaluate(status);
|
||||
var readyStatus = readiness.IsReady ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||
await WriteHealthAsync(context, readyStatus, "ready", status, readiness);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.Equals("/health/startup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var startupStatus = status.IsStarted ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
|
||||
await WriteHealthAsync(context, startupStatus, "startup", status);
|
||||
await WriteHealthAsync(context, startupStatus, "startup", status, readinessEvaluator.Evaluate(status));
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "ok", status);
|
||||
await WriteHealthAsync(context, StatusCodes.Status200OK, "ok", status, readinessEvaluator.Evaluate(status));
|
||||
}
|
||||
|
||||
private static Task WriteHealthAsync(HttpContext context, int statusCode, string status, GatewayServiceStatus serviceStatus)
|
||||
private static Task WriteHealthAsync(
|
||||
HttpContext context,
|
||||
int statusCode,
|
||||
string status,
|
||||
GatewayServiceStatus serviceStatus,
|
||||
GatewayReadinessReport readiness)
|
||||
{
|
||||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
@@ -67,7 +77,10 @@ public sealed class HealthCheckMiddleware
|
||||
{
|
||||
status,
|
||||
started = serviceStatus.IsStarted,
|
||||
ready = serviceStatus.IsReady,
|
||||
ready = readiness.IsReady,
|
||||
transportReady = serviceStatus.IsReady,
|
||||
requiredMicroservices = readiness.RequiredMicroservices,
|
||||
missingMicroservices = readiness.MissingMicroservices,
|
||||
traceId = context.TraceIdentifier
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using StellaOps.Gateway.WebService.Configuration;
|
||||
@@ -47,7 +48,7 @@ public sealed class RouteDispatchMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
var route = _resolver.Resolve(context.Request.Path);
|
||||
var (route, regexMatch) = _resolver.Resolve(context.Request.Path);
|
||||
if (route is null)
|
||||
{
|
||||
await _next(context);
|
||||
@@ -83,13 +84,13 @@ public sealed class RouteDispatchMiddleware
|
||||
await HandleStaticFile(context, route);
|
||||
break;
|
||||
case StellaOpsRouteType.ReverseProxy:
|
||||
await HandleReverseProxy(context, route);
|
||||
await HandleReverseProxy(context, route, regexMatch);
|
||||
break;
|
||||
case StellaOpsRouteType.WebSocket:
|
||||
await HandleWebSocket(context, route);
|
||||
break;
|
||||
case StellaOpsRouteType.Microservice:
|
||||
PrepareMicroserviceRoute(context, route);
|
||||
PrepareMicroserviceRoute(context, route, regexMatch);
|
||||
await _next(context);
|
||||
break;
|
||||
default:
|
||||
@@ -178,17 +179,24 @@ public sealed class RouteDispatchMiddleware
|
||||
await stream.CopyToAsync(context.Response.Body, context.RequestAborted);
|
||||
}
|
||||
|
||||
private async Task HandleReverseProxy(HttpContext context, StellaOpsRoute route)
|
||||
private async Task HandleReverseProxy(HttpContext context, StellaOpsRoute route, Match? regexMatch)
|
||||
{
|
||||
var requestPath = context.Request.Path.Value ?? string.Empty;
|
||||
var resolvedTranslatesTo = ResolveCaptureGroups(route.TranslatesTo, regexMatch);
|
||||
var captureGroupsResolved = !string.Equals(resolvedTranslatesTo, route.TranslatesTo, StringComparison.Ordinal);
|
||||
var remainingPath = requestPath;
|
||||
|
||||
if (!route.IsRegex && requestPath.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase))
|
||||
if (captureGroupsResolved)
|
||||
{
|
||||
// Capture groups resolved: TranslatesTo already contains the full target path.
|
||||
remainingPath = string.Empty;
|
||||
}
|
||||
else if (!route.IsRegex && requestPath.StartsWith(route.Path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
remainingPath = requestPath[route.Path.Length..];
|
||||
}
|
||||
|
||||
var upstreamBase = route.TranslatesTo!.TrimEnd('/');
|
||||
var upstreamBase = resolvedTranslatesTo!.TrimEnd('/');
|
||||
var upstreamUri = new Uri($"{upstreamBase}{remainingPath}{context.Request.QueryString}");
|
||||
|
||||
var client = _httpClientFactory.CreateClient("RouteDispatch");
|
||||
@@ -279,15 +287,34 @@ public sealed class RouteDispatchMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrepareMicroserviceRoute(HttpContext context, StellaOpsRoute route)
|
||||
private static void PrepareMicroserviceRoute(HttpContext context, StellaOpsRoute route, Match? regexMatch)
|
||||
{
|
||||
var translatedPath = ResolveTranslatedMicroservicePath(context.Request.Path.Value, route);
|
||||
// If regex route with capture groups, resolve $1/$2/etc. in TranslatesTo
|
||||
var effectiveRoute = route;
|
||||
if (regexMatch is not null && !string.IsNullOrWhiteSpace(route.TranslatesTo))
|
||||
{
|
||||
var resolvedTranslatesTo = ResolveCaptureGroups(route.TranslatesTo, regexMatch);
|
||||
if (!string.Equals(resolvedTranslatesTo, route.TranslatesTo, StringComparison.Ordinal))
|
||||
{
|
||||
effectiveRoute = new StellaOpsRoute
|
||||
{
|
||||
Type = route.Type,
|
||||
Path = route.Path,
|
||||
IsRegex = route.IsRegex,
|
||||
TranslatesTo = resolvedTranslatesTo,
|
||||
DefaultTimeout = route.DefaultTimeout,
|
||||
PreserveAuthHeaders = route.PreserveAuthHeaders
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var translatedPath = ResolveTranslatedMicroservicePath(context.Request.Path.Value, effectiveRoute);
|
||||
if (!string.Equals(translatedPath, context.Request.Path.Value, StringComparison.Ordinal))
|
||||
{
|
||||
context.Items[RouterHttpContextKeys.TranslatedRequestPath] = translatedPath;
|
||||
}
|
||||
|
||||
var targetMicroservice = ResolveRouteTargetMicroservice(route);
|
||||
var targetMicroservice = ResolveRouteTargetMicroservice(effectiveRoute);
|
||||
if (!string.IsNullOrWhiteSpace(targetMicroservice))
|
||||
{
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] = targetMicroservice;
|
||||
@@ -300,6 +327,22 @@ public sealed class RouteDispatchMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveCaptureGroups(string? translatesTo, Match? regexMatch)
|
||||
{
|
||||
if (regexMatch is null || string.IsNullOrWhiteSpace(translatesTo))
|
||||
{
|
||||
return translatesTo;
|
||||
}
|
||||
|
||||
var resolved = translatesTo;
|
||||
for (var i = regexMatch.Groups.Count - 1; i >= 1; i--)
|
||||
{
|
||||
resolved = resolved.Replace($"${i}", regexMatch.Groups[i].Value);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static string ResolveTranslatedMicroservicePath(string? requestPathValue, StellaOpsRoute route)
|
||||
{
|
||||
var requestPath = string.IsNullOrWhiteSpace(requestPathValue) ? "/" : requestPathValue!;
|
||||
@@ -314,12 +357,18 @@ public sealed class RouteDispatchMiddleware
|
||||
return requestPath;
|
||||
}
|
||||
|
||||
// For regex routes, the TranslatesTo (after capture group substitution)
|
||||
// already contains the full target path. Use it directly.
|
||||
if (route.IsRegex)
|
||||
{
|
||||
return NormalizePath(targetPrefix);
|
||||
}
|
||||
|
||||
var normalizedRoutePath = NormalizePath(route.Path);
|
||||
var normalizedRequestPath = NormalizePath(requestPath);
|
||||
var remainingPath = normalizedRequestPath;
|
||||
|
||||
if (!route.IsRegex &&
|
||||
normalizedRequestPath.StartsWith(normalizedRoutePath, StringComparison.OrdinalIgnoreCase))
|
||||
if (normalizedRequestPath.StartsWith(normalizedRoutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
remainingPath = normalizedRequestPath[normalizedRoutePath.Length..];
|
||||
if (!remainingPath.StartsWith('/'))
|
||||
|
||||
Reference in New Issue
Block a user