feat(stability): unify readiness and recovery
CI - Build & Test / Backend (.NET) (push) Successful in 45s
CI - Build & Test / Backend integration (PostgreSQL/Toxiproxy) (push) Failing after 1m0s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m49s
CI - Build & Test / Security Check (push) Successful in 7s
CI - Build & Test / Deploy Nexus (push) Has been skipped

This commit is contained in:
AzuTear
2026-08-01 01:21:33 +02:00
parent 38282e4f7f
commit cd8c78d165
67 changed files with 2616 additions and 601 deletions
+137
View File
@@ -0,0 +1,137 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.WebUtilities;
namespace Nexus.Api.Http;
public static class NexusProblemCodes
{
public const string ValidationFailed = "validation_failed";
public const string Unauthenticated = "unauthenticated";
public const string Forbidden = "forbidden";
public const string NotFound = "not_found";
public const string Conflict = "conflict";
public const string UnsupportedCapability = "unsupported_capability";
public const string DependencyUnavailable = "dependency_unavailable";
public const string Timeout = "timeout";
public const string RateLimited = "rate_limited";
public const string InternalError = "internal_error";
public static string ForStatus(int statusCode) => statusCode switch
{
StatusCodes.Status400BadRequest or StatusCodes.Status422UnprocessableEntity => ValidationFailed,
StatusCodes.Status401Unauthorized => Unauthenticated,
StatusCodes.Status403Forbidden => Forbidden,
StatusCodes.Status404NotFound => NotFound,
StatusCodes.Status409Conflict => Conflict,
StatusCodes.Status429TooManyRequests => RateLimited,
StatusCodes.Status501NotImplemented => UnsupportedCapability,
StatusCodes.Status502BadGateway or StatusCodes.Status503ServiceUnavailable => DependencyUnavailable,
StatusCodes.Status504GatewayTimeout => Timeout,
_ => InternalError
};
}
public static class NexusProblemDetailsDefaults
{
public static void Apply(ProblemDetails problem, HttpContext httpContext)
{
var statusCode = problem.Status
?? (httpContext.Response.StatusCode >= 400
? httpContext.Response.StatusCode
: StatusCodes.Status500InternalServerError);
problem.Status = statusCode;
problem.Type ??= $"https://httpstatuses.com/{statusCode}";
problem.Title ??= ReasonPhrases.GetReasonPhrase(statusCode);
problem.Extensions.TryAdd("code", NexusProblemCodes.ForStatus(statusCode));
problem.Extensions.TryAdd(
"traceId",
Activity.Current?.Id ?? httpContext.TraceIdentifier);
}
public static ProblemDetails? FromLegacyError(
object? value,
int? statusCode,
HttpContext httpContext)
{
if (value is null || statusCode is null || statusCode < 400)
return null;
var errorProperty = value.GetType().GetProperty(
"error",
System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.IgnoreCase);
if (errorProperty?.GetValue(value) is not string detail
|| string.IsNullOrWhiteSpace(detail))
{
return null;
}
var problem = new ProblemDetails
{
Status = statusCode,
Detail = detail
};
Apply(problem, httpContext);
return problem;
}
}
public static class NexusHttpResults
{
public static IResult Problem(
int statusCode,
string detail,
string? code = null,
string? title = null,
IDictionary<string, object?>? extensions = null)
{
var values = extensions is null
? new Dictionary<string, object?>()
: new Dictionary<string, object?>(extensions);
values.TryAdd("code", code ?? NexusProblemCodes.ForStatus(statusCode));
return Results.Problem(
statusCode: statusCode,
title: title,
detail: detail,
extensions: values);
}
}
/// <summary>
/// Ensures MVC-produced ProblemDetails values use the same extensions as the
/// global exception and status-code writers.
/// </summary>
public sealed class NexusProblemDetailsFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is not ObjectResult objectResult)
return;
if (objectResult.Value is ProblemDetails problem)
{
NexusProblemDetailsDefaults.Apply(problem, context.HttpContext);
return;
}
var legacyProblem = NexusProblemDetailsDefaults.FromLegacyError(
objectResult.Value,
objectResult.StatusCode,
context.HttpContext);
if (legacyProblem is null)
return;
objectResult.Value = legacyProblem;
objectResult.ContentTypes.Clear();
objectResult.ContentTypes.Add("application/problem+json");
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}