classlib/ExpenseTracker.Api/Middlewares/GlobalExceptionHandlerMiddleware.cs
2024-08-07 21:12:02 +03:00

213 lines
8.7 KiB
C#

using ExpenseTracker.Application.Common.Exceptions;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace ExpenseTracker.Api.Middlewares;
public class GlobalExceptionHandlerMiddleware : IMiddleware
{
class ProblemDetailsWithTraceId : ProblemDetails
{
public string TraceId { get; init; } = Activity.Current?.TraceId.ToString();
}
private readonly Dictionary<Type, Func<HttpContext, Exception, Task>> _exceptionHandlers;
private readonly ILogger _logger;
public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger)
{
// Register known exception types and handlers.
_exceptionHandlers = new()
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(RegistrationException), HandleRegistrationException },
{ typeof(LoginException), HandleLoginException },
{ typeof(RenewAccessTokenException), HandleRenewAccessTokenException },
{ typeof(RevokeRefreshTokenException), HandleRevokeRefreshTokenException },
{ typeof(NotFoundException), HandleNotFoundException },
{ typeof(UnAuthorizedException), HandleUnAuthorizedException },
{ typeof(ForbiddenException), HandleForbiddenException },
{ typeof(CurrencyConverterException), HandleCurrencyConverterException },
};
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception exception)
{
var exceptionType = exception.GetType();
_logger.LogInformation(
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Interrupted with {@ExceptionType}.",
DateTime.UtcNow.ToString("yyyy-MM-dd"),
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
Activity.Current?.TraceId.ToString(),
Activity.Current?.SpanId.ToString(),
exceptionType);
if (_exceptionHandlers.ContainsKey(exceptionType))
{
await _exceptionHandlers[exceptionType].Invoke(context, exception);
return;
}
await HandleUnhandledExceptionException(context, exception);
}
}
private async Task HandleValidationException(HttpContext context, Exception exception)
{
var ex = (ValidationException)exception;
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails(ex.Errors)
{
Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
Title = "One or more validation errors occurred.",
Detail = "Provided data doesn't satisfy validation requirements."
});
}
private async Task HandleRegistrationException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request",
Title = "Registration failed.",
Detail = "Check your credentials."
});
}
private async Task HandleLoginException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request",
Title = "Login failed.",
Detail = "Provided email and/or password are invalid."
});
}
private async Task HandleRenewAccessTokenException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request",
Title = "Access token renewal failed.",
Detail = "Check validity of your refresh token."
});
}
private async Task HandleRevokeRefreshTokenException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request",
Title = "Refresh token revocation failed.",
Detail = "Check validity of your refresh token."
});
}
private async Task HandleNotFoundException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status404NotFound,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-404-not-found",
Title = "One or more resources was not found.",
Detail = "Check validity of input data."
});
}
private async Task HandleUnAuthorizedException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status401Unauthorized,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized",
Title = "Request lacks valid authentication credentials for the target resource.",
});
}
private async Task HandleForbiddenException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status403Forbidden,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-403-forbidden",
Title = "Server refuses to fulfill the request.",
});
}
private async Task HandleCurrencyConverterException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status500InternalServerError,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-500-internal-server-error",
Title = "Unable to communicate with currency exchage api.",
Detail = "Use default currency to query items without conversion."
});
}
private async Task HandleUnhandledExceptionException(HttpContext context, Exception exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{
Status = StatusCodes.Status500InternalServerError,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#name-500-internal-server-error",
Title = "One or more internal server errors occured.",
Detail = "Report this error to service's support team.",
});
_logger.LogError(
"{@DateUtc} {@TimeUtc} {@TraceId} {@SpanId} Unhandled exception.\n{@ExceptionMessage}.\n{@ExceptionStackTrace}.",
DateTime.UtcNow.ToString("yyyy-MM-dd"),
DateTime.UtcNow.ToString("HH:mm:ss.FFF"),
Activity.Current?.TraceId.ToString(),
Activity.Current?.SpanId.ToString(),
exception.Message,
exception.StackTrace);
}
}