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? extensions = null) { var values = extensions is null ? new Dictionary() : new Dictionary(extensions); values.TryAdd("code", code ?? NexusProblemCodes.ForStatus(statusCode)); return Results.Problem( statusCode: statusCode, title: title, detail: detail, extensions: values); } } /// /// Ensures MVC-produced ProblemDetails values use the same extensions as the /// global exception and status-code writers. /// 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) { } }