213 lines
8.7 KiB
C#
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);
|
|
}
|
|
}
|