feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction

- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST.
- Added command-line interface with options for JSON output and help.
- Included functionality to analyze project structure, detect functions, and build call graphs.
- Created a package.json file for dependency management.

feat: introduce stella-callgraph-python for Python call graph extraction

- Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis.
- Implemented command-line interface with options for JSON output and verbose logging.
- Added framework detection to identify popular web frameworks and their entry points.
- Created an AST analyzer to traverse Python code and extract function definitions and calls.
- Included requirements.txt for project dependencies.

chore: add framework detection for Python projects

- Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns.
- Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
master
2025-12-19 18:11:59 +02:00
parent 951a38d561
commit 8779e9226f
130 changed files with 19011 additions and 422 deletions

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Router.Common.Models;
@@ -12,6 +13,7 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
private readonly StellaMicroserviceOptions _options;
private readonly ILogger<GeneratedEndpointDiscoveryProvider> _logger;
private readonly ReflectionEndpointDiscoveryProvider _reflectionFallback;
private readonly IServiceProviderIsService? _serviceProviderIsService;
private const string GeneratedProviderTypeName = "StellaOps.Microservice.Generated.GeneratedEndpointProvider";
@@ -20,11 +22,16 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
/// </summary>
public GeneratedEndpointDiscoveryProvider(
StellaMicroserviceOptions options,
ILogger<GeneratedEndpointDiscoveryProvider> logger)
ILogger<GeneratedEndpointDiscoveryProvider> logger,
IServiceProviderIsService? serviceProviderIsService = null)
{
_options = options;
_logger = logger;
_reflectionFallback = new ReflectionEndpointDiscoveryProvider(options);
_serviceProviderIsService = serviceProviderIsService;
_reflectionFallback = new ReflectionEndpointDiscoveryProvider(
options,
assemblies: null,
serviceProviderIsService: serviceProviderIsService);
}
/// <inheritdoc />
@@ -65,33 +72,38 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
{
try
{
// Look in the entry assembly first
var entryAssembly = Assembly.GetEntryAssembly();
var providerType = entryAssembly?.GetType(GeneratedProviderTypeName);
if (providerType != null)
static Type? GetProviderType(Assembly? assembly)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
if (assembly is null) return null;
var type = assembly.GetType(GeneratedProviderTypeName);
if (type is null) return null;
return typeof(IGeneratedEndpointProvider).IsAssignableFrom(type) ? type : null;
}
// Also check the calling assembly
var callingAssembly = Assembly.GetCallingAssembly();
providerType = callingAssembly.GetType(GeneratedProviderTypeName);
var providerTypes = new List<Type>();
if (providerType != null)
// Look in the entry and calling assemblies first.
var entryProviderType = GetProviderType(Assembly.GetEntryAssembly());
if (entryProviderType is not null)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
providerTypes.Add(entryProviderType);
}
// Check all loaded assemblies
var callingProviderType = GetProviderType(Assembly.GetCallingAssembly());
if (callingProviderType is not null)
{
providerTypes.Add(callingProviderType);
}
// Check all loaded assemblies (integration tests may host multiple microservices in one process).
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
providerType = assembly.GetType(GeneratedProviderTypeName);
if (providerType != null)
var providerType = GetProviderType(assembly);
if (providerType is not null)
{
return (IGeneratedEndpointProvider)Activator.CreateInstance(providerType)!;
providerTypes.Add(providerType);
}
}
catch
@@ -99,6 +111,78 @@ public sealed class GeneratedEndpointDiscoveryProvider : IEndpointDiscoveryProvi
// Ignore assembly loading errors
}
}
providerTypes = providerTypes.Distinct().ToList();
if (providerTypes.Count == 0)
{
return null;
}
// If multiple providers exist (e.g., test process hosts several microservices), pick the provider that
// best matches the currently-registered handler types in DI.
if (_serviceProviderIsService is not null && providerTypes.Count > 1)
{
IGeneratedEndpointProvider? best = null;
var bestScore = 0;
foreach (var providerType in providerTypes)
{
IGeneratedEndpointProvider? providerInstance;
try
{
providerInstance =
(IGeneratedEndpointProvider?)Activator.CreateInstance(providerType, nonPublic: true);
}
catch
{
continue;
}
if (providerInstance is null)
{
continue;
}
var score = 0;
try
{
foreach (var handlerType in providerInstance.GetHandlerTypes())
{
if (_serviceProviderIsService.IsService(handlerType))
{
score++;
}
}
}
catch
{
score = 0;
}
if (score > bestScore)
{
bestScore = score;
best = providerInstance;
}
}
if (best is not null && bestScore > 0)
{
_logger.LogDebug(
"Selected generated endpoint provider {ProviderType} (matched {MatchCount} handlers)",
best.GetType().FullName,
bestScore);
return best;
}
}
// Deterministic fallback: choose the provider type with the lowest assembly name.
var selectedProviderType = providerTypes
.OrderBy(t => t.Assembly.GetName().Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(t => t.FullName, StringComparer.Ordinal)
.First();
return (IGeneratedEndpointProvider)Activator.CreateInstance(selectedProviderType, nonPublic: true)!;
}
catch (Exception ex)
{

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Router.Common.Models;
namespace StellaOps.Microservice;
@@ -10,16 +11,21 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
{
private readonly StellaMicroserviceOptions _options;
private readonly IEnumerable<Assembly> _assemblies;
private readonly IServiceProviderIsService? _serviceProviderIsService;
/// <summary>
/// Initializes a new instance of the <see cref="ReflectionEndpointDiscoveryProvider"/> class.
/// </summary>
/// <param name="options">The microservice options.</param>
/// <param name="assemblies">The assemblies to scan for endpoints.</param>
public ReflectionEndpointDiscoveryProvider(StellaMicroserviceOptions options, IEnumerable<Assembly>? assemblies = null)
public ReflectionEndpointDiscoveryProvider(
StellaMicroserviceOptions options,
IEnumerable<Assembly>? assemblies = null,
IServiceProviderIsService? serviceProviderIsService = null)
{
_options = options;
_assemblies = assemblies ?? AppDomain.CurrentDomain.GetAssemblies();
_serviceProviderIsService = serviceProviderIsService;
}
/// <inheritdoc />
@@ -42,6 +48,11 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
$"Type {type.FullName} has [StellaEndpoint] but does not implement IStellaEndpoint.");
}
if (_serviceProviderIsService is not null && !_serviceProviderIsService.IsService(type))
{
continue;
}
var claims = attribute.RequiredClaims
.Select(c => new ClaimRequirement { Type = c })
.ToList();
@@ -54,7 +65,8 @@ public sealed class ReflectionEndpointDiscoveryProvider : IEndpointDiscoveryProv
Path = attribute.Path,
DefaultTimeout = TimeSpan.FromSeconds(attribute.TimeoutSeconds),
SupportsStreaming = attribute.SupportsStreaming,
RequiringClaims = claims
RequiringClaims = claims,
HandlerType = type
};
endpoints.Add(descriptor);

View File

@@ -15,7 +15,7 @@
## Working Directory & Scope
- Primary: `src/__Libraries/StellaOps.Router.Gateway`
- Allowed tests: `src/__Libraries/__Tests/StellaOps.Router.Gateway.Tests`
- Allowed tests: `tests/StellaOps.Router.Gateway.Tests`
- Allowed shared dependencies (read/consume): `src/__Libraries/StellaOps.Router.Common`, `src/__Libraries/StellaOps.Router.Config`, `src/__Libraries/StellaOps.Router.Transport.*`
- Cross-module edits require a note in the owning sprints **Execution Log** and **Decisions & Risks**.
@@ -28,7 +28,7 @@
## Testing Expectations
- Add/modify unit tests for every behavior change.
- Prefer unit tests for config parsing, route matching, and limiter logic; keep integration tests behind explicit opt-in when they require Docker/Valkey.
- Default command: `dotnet test src/__Libraries/__Tests/StellaOps.Router.Gateway.Tests -c Release`.
- Default command: `dotnet test tests/StellaOps.Router.Gateway.Tests/StellaOps.Router.Gateway.Tests.csproj -c Release`.
## Handoff Notes
- Keep this file aligned with router architecture docs and sprint decisions; record updates in sprint **Execution Log**.

View File

@@ -16,6 +16,10 @@ public static class ApplicationBuilderExtensions
/// <returns>The application builder for chaining.</returns>
public static IApplicationBuilder UseRouterGateway(this IApplicationBuilder app)
{
// Request logging and error handling wrap the full router pipeline.
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<GlobalErrorHandlerMiddleware>();
// Enforce payload limits first
app.UseMiddleware<PayloadLimitsMiddleware>();
@@ -60,6 +64,10 @@ public static class ApplicationBuilderExtensions
/// <returns>The application builder for chaining.</returns>
public static IApplicationBuilder UseRouterGatewayCore(this IApplicationBuilder app)
{
// Request logging and error handling wrap the full router pipeline.
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<GlobalErrorHandlerMiddleware>();
// Resolve endpoints from routing state
app.UseMiddleware<EndpointResolutionMiddleware>();

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.Middleware;
namespace StellaOps.Router.Gateway.Authorization;
@@ -70,14 +71,19 @@ public sealed class AuthorizationMiddleware
required.Type,
required.Value ?? "(any)");
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = "Forbidden",
message = "Authorization failed: missing required claim",
requiredClaim = new { type = required.Type, value = required.Value }
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status403Forbidden,
error: "Forbidden",
message: "Authorization failed: missing required claim",
service: endpoint.ServiceName,
version: endpoint.Version,
details: new Dictionary<string, object?>
{
["requiredClaimType"] = required.Type,
["requiredClaimValue"] = required.Value
},
cancellationToken: context.RequestAborted);
return;
}
}

View File

@@ -10,7 +10,12 @@ public sealed class RouterNodeConfig
/// <summary>
/// Configuration section name for binding.
/// </summary>
public const string SectionName = "Router:Node";
public const string SectionName = "GatewayNode";
/// <summary>
/// Legacy configuration section name (deprecated).
/// </summary>
public const string LegacySectionName = "Router:Node";
/// <summary>
/// Gets or sets the region where this gateway is deployed (e.g., "eu1").

View File

@@ -25,9 +25,32 @@ public static class RouterServiceCollectionExtensions
this IServiceCollection services,
IConfiguration configuration)
{
// Bind configuration options
services.Configure<RouterNodeConfig>(
configuration.GetSection(RouterNodeConfig.SectionName));
var nodeSection = configuration.GetSection(RouterNodeConfig.SectionName);
if (!nodeSection.Exists())
{
var legacyNodeSection = configuration.GetSection(RouterNodeConfig.LegacySectionName);
if (legacyNodeSection.Exists())
{
nodeSection = legacyNodeSection;
}
}
// Bind configuration options (fail-fast validation on startup).
services.AddOptions<RouterNodeConfig>()
.Bind(nodeSection)
.ValidateDataAnnotations()
.Validate(
config => !string.IsNullOrWhiteSpace(config.Region),
$"{RouterNodeConfig.SectionName} (or {RouterNodeConfig.LegacySectionName}):Region is required. Gateway cannot start without a region assignment.")
.ValidateOnStart();
services.PostConfigure<RouterNodeConfig>(config =>
{
if (string.IsNullOrWhiteSpace(config.NodeId) && !string.IsNullOrWhiteSpace(config.Region))
{
var suffix = Guid.NewGuid().ToString("N")[..8];
config.NodeId = $"gw-{config.Region}-{suffix}";
}
});
services.Configure<RoutingOptions>(
configuration.GetSection(RoutingOptions.SectionName));
services.Configure<HealthOptions>(
@@ -75,7 +98,21 @@ public static class RouterServiceCollectionExtensions
Action<RoutingOptions>? configureRouting = null)
{
// Ensure default options are registered even if no configuration action provided
services.AddOptions<RouterNodeConfig>();
services.AddOptions<RouterNodeConfig>()
.ValidateDataAnnotations()
.Validate(
config => !string.IsNullOrWhiteSpace(config.Region),
$"{RouterNodeConfig.SectionName}:Region is required. Gateway cannot start without a region assignment.")
.ValidateOnStart();
services.PostConfigure<RouterNodeConfig>(config =>
{
if (string.IsNullOrWhiteSpace(config.NodeId) && !string.IsNullOrWhiteSpace(config.Region))
{
var suffix = Guid.NewGuid().ToString("N")[..8];
config.NodeId = $"gw-{config.Region}-{suffix}";
}
});
services.AddOptions<RoutingOptions>();
services.AddOptions<HealthOptions>();
services.AddOptions<PayloadLimits>();
@@ -111,7 +148,20 @@ public static class RouterServiceCollectionExtensions
public static IServiceCollection AddRouterGatewayCore(this IServiceCollection services)
{
// Register options with defaults
services.AddOptions<RouterNodeConfig>();
services.AddOptions<RouterNodeConfig>()
.ValidateDataAnnotations()
.Validate(
config => !string.IsNullOrWhiteSpace(config.Region),
$"{RouterNodeConfig.SectionName}:Region is required. Gateway cannot start without a region assignment.")
.ValidateOnStart();
services.PostConfigure<RouterNodeConfig>(config =>
{
if (string.IsNullOrWhiteSpace(config.NodeId) && !string.IsNullOrWhiteSpace(config.Region))
{
var suffix = Guid.NewGuid().ToString("N")[..8];
config.NodeId = $"gw-{config.Region}-{suffix}";
}
});
services.AddOptions<RoutingOptions>();
services.AddOptions<HealthOptions>();
services.AddOptions<PayloadLimits>();

View File

@@ -28,13 +28,11 @@ public sealed class EndpointResolutionMiddleware
var endpoint = routingState.ResolveEndpoint(method, path);
if (endpoint is null)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsJsonAsync(new
{
error = "Endpoint not found",
method,
path
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status404NotFound,
error: "Endpoint not found",
cancellationToken: context.RequestAborted);
return;
}

View File

@@ -0,0 +1,58 @@
using Microsoft.Extensions.Hosting;
namespace StellaOps.Router.Gateway.Middleware;
/// <summary>
/// Catches unhandled exceptions and returns a structured JSON error response.
/// </summary>
public sealed class GlobalErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalErrorHandlerMiddleware> _logger;
private readonly IHostEnvironment _environment;
public GlobalErrorHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalErrorHandlerMiddleware> logger,
IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
{
_logger.LogDebug(
"Request cancelled by client: {Method} {Path} TraceId={TraceId}",
context.Request.Method,
context.Request.Path,
context.TraceIdentifier);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Unhandled gateway exception: {Method} {Path} TraceId={TraceId}",
context.Request.Method,
context.Request.Path,
context.TraceIdentifier);
var message = _environment.IsDevelopment() ? ex.Message : null;
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status500InternalServerError,
error: "Internal Server Error",
message: message,
cancellationToken: context.RequestAborted);
}
}
}

View File

@@ -44,12 +44,16 @@ public sealed class PayloadLimitsMiddleware
connectionId);
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
await context.Response.WriteAsJsonAsync(new
{
error = "Payload Too Large",
maxBytes = _limits.MaxRequestBytesPerCall,
contentLength = context.Request.ContentLength.Value
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status413PayloadTooLarge,
error: "Payload Too Large",
details: new Dictionary<string, object?>
{
["maxBytes"] = _limits.MaxRequestBytesPerCall,
["contentLength"] = context.Request.ContentLength.Value
},
cancellationToken: context.RequestAborted);
return;
}
@@ -67,11 +71,12 @@ public sealed class PayloadLimitsMiddleware
connectionId);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(new
{
error = "Service Overloaded",
message = "Too many concurrent requests"
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status503ServiceUnavailable,
error: "Service Overloaded",
message: "Too many concurrent requests",
cancellationToken: context.RequestAborted);
}
else
{
@@ -83,11 +88,12 @@ public sealed class PayloadLimitsMiddleware
_limits.MaxRequestBytesPerConnection);
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsJsonAsync(new
{
error = "Too Many Requests",
message = "Per-connection limit exceeded"
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status429TooManyRequests,
error: "Too Many Requests",
message: "Per-connection limit exceeded",
cancellationToken: context.RequestAborted);
}
return;
@@ -139,12 +145,16 @@ public sealed class PayloadLimitsMiddleware
if (!context.Response.HasStarted)
{
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
await context.Response.WriteAsJsonAsync(new
{
error = "Payload Too Large",
maxBytes = _limits.MaxRequestBytesPerCall,
bytesReceived = ex.BytesRead
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status413PayloadTooLarge,
error: "Payload Too Large",
details: new Dictionary<string, object?>
{
["maxBytes"] = _limits.MaxRequestBytesPerCall,
["bytesReceived"] = ex.BytesRead
},
cancellationToken: context.RequestAborted);
}
actualBytesRead = ex.BytesRead;

View File

@@ -0,0 +1,38 @@
using System.Diagnostics;
namespace StellaOps.Router.Gateway.Middleware;
/// <summary>
/// Logs basic request/response information for gateway requests.
/// </summary>
public sealed class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
var start = Stopwatch.GetTimestamp();
await _next(context);
var elapsed = Stopwatch.GetElapsedTime(start);
context.Items.TryGetValue(RouterHttpContextKeys.TargetMicroservice, out var targetServiceObj);
_logger.LogInformation(
"HTTP {Method} {Path} -> {StatusCode} in {ElapsedMs}ms TraceId={TraceId} TargetService={TargetService}",
context.Request.Method,
context.Request.Path + context.Request.QueryString,
context.Response.StatusCode,
elapsed.TotalMilliseconds,
context.TraceIdentifier,
targetServiceObj as string ?? "(none)");
}
}

View File

@@ -0,0 +1,54 @@
namespace StellaOps.Router.Gateway.Middleware;
internal sealed record RouterErrorResponse
{
public required string Error { get; init; }
public required int Status { get; init; }
public required string TraceId { get; init; }
public required string Method { get; init; }
public required string Path { get; init; }
public string? Service { get; init; }
public string? Version { get; init; }
public string? Message { get; init; }
public IReadOnlyDictionary<string, object?>? Details { get; init; }
}
internal static class RouterErrorWriter
{
public static Task WriteAsync(
HttpContext context,
int statusCode,
string error,
string? message = null,
string? service = null,
string? version = null,
IReadOnlyDictionary<string, object?>? details = null,
CancellationToken cancellationToken = default)
{
if (context.Response.HasStarted)
{
return Task.CompletedTask;
}
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8";
var response = new RouterErrorResponse
{
Error = error,
Status = statusCode,
TraceId = context.TraceIdentifier,
Method = context.Request.Method,
Path = context.Request.Path + context.Request.QueryString,
Message = message,
Service = service,
Version = version,
Details = details
};
return context.Response.WriteAsJsonAsync(response, cancellationToken: cancellationToken);
}
}

View File

@@ -33,8 +33,11 @@ public sealed class RoutingDecisionMiddleware
var endpoint = context.Items[RouterHttpContextKeys.EndpointDescriptor] as EndpointDescriptor;
if (endpoint is null)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Endpoint descriptor missing" });
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status500InternalServerError,
error: "Endpoint descriptor missing",
cancellationToken: context.RequestAborted);
return;
}
@@ -66,13 +69,13 @@ public sealed class RoutingDecisionMiddleware
if (decision is null)
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(new
{
error = "No instances available",
service = endpoint.ServiceName,
version = endpoint.Version
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status503ServiceUnavailable,
error: "No instances available",
service: endpoint.ServiceName,
version: endpoint.Version,
cancellationToken: context.RequestAborted);
return;
}

View File

@@ -70,8 +70,11 @@ public sealed class TransportDispatchMiddleware
var decision = context.Items[RouterHttpContextKeys.RoutingDecision] as RoutingDecision;
if (decision is null)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Routing decision missing" });
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status500InternalServerError,
error: "Routing decision missing",
cancellationToken: context.RequestAborted);
return;
}
@@ -196,12 +199,16 @@ public sealed class TransportDispatchMiddleware
}
context.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
await context.Response.WriteAsJsonAsync(new
{
error = "Upstream timeout",
service = decision.Connection.Instance.ServiceName,
timeout = decision.EffectiveTimeout.TotalSeconds
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status504GatewayTimeout,
error: "Upstream timeout",
service: decision.Connection.Instance.ServiceName,
details: new Dictionary<string, object?>
{
["timeoutSeconds"] = decision.EffectiveTimeout.TotalSeconds
},
cancellationToken: context.RequestAborted);
return;
}
catch (OperationCanceledException)
@@ -219,11 +226,12 @@ public sealed class TransportDispatchMiddleware
decision.Connection.Instance.ServiceName);
context.Response.StatusCode = StatusCodes.Status502BadGateway;
await context.Response.WriteAsJsonAsync(new
{
error = "Upstream error",
message = ex.Message
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status502BadGateway,
error: "Upstream error",
message: ex.Message,
cancellationToken: context.RequestAborted);
return;
}
@@ -244,7 +252,11 @@ public sealed class TransportDispatchMiddleware
requestId);
context.Response.StatusCode = StatusCodes.Status502BadGateway;
await context.Response.WriteAsJsonAsync(new { error = "Invalid upstream response" });
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status502BadGateway,
error: "Invalid upstream response",
cancellationToken: context.RequestAborted);
return;
}
@@ -423,12 +435,16 @@ public sealed class TransportDispatchMiddleware
if (!responseReceived)
{
context.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
await context.Response.WriteAsJsonAsync(new
{
error = "Upstream streaming timeout",
service = decision.Connection.Instance.ServiceName,
timeout = decision.EffectiveTimeout.TotalSeconds
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status504GatewayTimeout,
error: "Upstream streaming timeout",
service: decision.Connection.Instance.ServiceName,
details: new Dictionary<string, object?>
{
["timeoutSeconds"] = decision.EffectiveTimeout.TotalSeconds
},
cancellationToken: context.RequestAborted);
}
}
catch (OperationCanceledException)
@@ -446,11 +462,12 @@ public sealed class TransportDispatchMiddleware
if (!responseReceived)
{
context.Response.StatusCode = StatusCodes.Status502BadGateway;
await context.Response.WriteAsJsonAsync(new
{
error = "Upstream streaming error",
message = ex.Message
});
await RouterErrorWriter.WriteAsync(
context,
statusCode: StatusCodes.Status502BadGateway,
error: "Upstream streaming error",
message: ex.Message,
cancellationToken: context.RequestAborted);
}
}
}