< Summary

Information
Class: ArturRios.Util.WebApi.Middleware.ExceptionMiddleware
Assembly: ArturRios.Util.WebApi
File(s): /home/runner/work/dotnet-webapi-util/dotnet-webapi-util/src/Middleware/ExceptionMiddleware.cs
Line coverage
82%
Covered lines: 23
Uncovered lines: 5
Coverable lines: 28
Total lines: 81
Line coverage: 82.1%
Branch coverage
83%
Covered branches: 5
Total branches: 6
Branch coverage: 83.3%
Method coverage
100%
Covered methods: 3
Fully covered methods: 1
Total methods: 3
Method coverage: 100%
Full method coverage: 33.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()100%1162.5%
HandleException()83.33%6689.47%

File(s)

/home/runner/work/dotnet-webapi-util/dotnet-webapi-util/src/Middleware/ExceptionMiddleware.cs

#LineLine coverage
 1using ArturRios.Output;
 2using ArturRios.Util.Http;
 3using ArturRios.Util.WebApi.EndpointToggle;
 4using ArturRios.Util.WebApi.Extensions;
 5using Microsoft.AspNetCore.Http;
 6using Microsoft.Extensions.Logging;
 7
 8namespace ArturRios.Util.WebApi.Middleware;
 9
 10/// <summary>
 11/// Catches unhandled exceptions raised further down the pipeline and converts them into a JSON error response,
 12/// logging the exception and quietly ignoring client-initiated request cancellations. An
 13/// <see cref="EndpointDisabledException"/> is answered with its own status code rather than 500. When the response
 14/// has already started, the exception is rethrown so the server aborts the response instead of leaving the client
 15/// with a truncated body that looks complete.
 16/// </summary>
 17/// <param name="next">The next middleware in the pipeline.</param>
 18/// <param name="logger">Used to log unhandled exceptions and cancellations.</param>
 3919public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger) : WebApiMiddleware
 20{
 21    private const string InternalServerError = "Internal server error, please try again later";
 22
 23    /// <summary>Invokes the next middleware, catching any unhandled exception and writing an error response instead of 
 24    /// <param name="httpContext">The current HTTP context.</param>
 25    public async Task InvokeAsync(HttpContext httpContext)
 26    {
 27        try
 28        {
 3429            await next(httpContext);
 2430        }
 031        catch (OperationCanceledException oce) when (httpContext.RequestAborted.IsCancellationRequested)
 32        {
 33            // TaskCanceledException derives from OperationCanceledException, so this covers both.
 034            logger.LogDebug("Request was canceled by the client: {Message}", oce.Message);
 035        }
 1036        catch (Exception ex) when (!httpContext.Response.HasStarted)
 37        {
 938            await HandleException(httpContext, ex);
 39        }
 3340    }
 41
 42    private async Task HandleException(HttpContext context, Exception exception)
 43    {
 944        if (context.RequestAborted.IsCancellationRequested)
 45        {
 046            logger.LogDebug(exception, "Cannot write an error response because the request was aborted.");
 47
 048            return;
 49        }
 50
 51        int statusCode;
 52        string[] errors;
 53
 54        switch (exception)
 55        {
 56            case EndpointDisabledException disabled:
 157                logger.LogInformation("Request reached a disabled endpoint: {Path}", context.Request.Path);
 158                statusCode = disabled.StatusCode;
 159                errors = disabled.Messages;
 160                break;
 61            case CustomException custom:
 162                logger.LogError(exception, "Unhandled exception while processing the request.");
 163                statusCode = HttpStatusCodes.InternalServerError;
 164                errors = custom.Messages;
 165                break;
 66            default:
 767                logger.LogError(exception, "Unhandled exception while processing the request.");
 768                statusCode = HttpStatusCodes.InternalServerError;
 769                errors = [InternalServerError];
 70                break;
 71        }
 72
 73        // Errors, not messages: Success is computed from Errors, so a 500 whose text sat under Messages
 74        // came back with "success": true. AuthenticationMiddleware already envelopes its 401 this way.
 975        var output = DataOutput<string>.New
 976            .WithData(string.Empty)
 977            .WithErrors(errors);
 78
 979        await context.Response.WriteOutputAsync(statusCode, output);
 980    }
 81}