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
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:
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user