79 lines
2.7 KiB
C#
79 lines
2.7 KiB
C#
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace ROLAC.API.Middleware;
|
|
|
|
/// <summary>
|
|
/// Catches any unhandled exception from the downstream pipeline, logs it (which flows through
|
|
/// the DB sink into SystemLogs at Error level with full stack + StatusCode 500), and returns a
|
|
/// clean RFC7807 problem+json response. Stack traces are never leaked to the client outside
|
|
/// Development. Registered as the FIRST middleware so it also catches auth/authorization faults.
|
|
/// </summary>
|
|
public sealed class ExceptionHandlingMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
|
private readonly IHostEnvironment _env;
|
|
|
|
public ExceptionHandlingMiddleware(
|
|
RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment env)
|
|
{
|
|
_next = next;
|
|
_logger = logger;
|
|
_env = env;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
try
|
|
{
|
|
await _next(context);
|
|
}
|
|
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
|
|
{
|
|
// The client went away — not a server error; don't log as 500.
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await HandleAsync(context, ex);
|
|
}
|
|
}
|
|
|
|
private async Task HandleAsync(HttpContext context, Exception exception)
|
|
{
|
|
// Logged here → picked up by the DB sink (Error ≥ Warning floor) with full ex.ToString().
|
|
_logger.LogError(
|
|
exception,
|
|
"Unhandled exception for {Method} {Path} (traceId {TraceId})",
|
|
context.Request.Method, context.Request.Path, context.TraceIdentifier);
|
|
|
|
if (context.Response.HasStarted)
|
|
{
|
|
// Too late to write a clean body; the log row above is still captured.
|
|
return;
|
|
}
|
|
|
|
var problem = new ProblemDetails
|
|
{
|
|
Status = StatusCodes.Status500InternalServerError,
|
|
Title = "An unexpected error occurred.",
|
|
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
|
|
};
|
|
problem.Extensions["traceId"] = context.TraceIdentifier;
|
|
if (_env.IsDevelopment())
|
|
problem.Detail = exception.ToString();
|
|
|
|
context.Response.Clear();
|
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
|
context.Response.ContentType = "application/problem+json";
|
|
|
|
await context.Response.WriteAsync(JsonSerializer.Serialize(problem, JsonSerializerOptions));
|
|
}
|
|
|
|
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
};
|
|
}
|