< Summary

Information
Class: ArturRios.Util.WebApi.Middleware.ExceptionMiddleware
Assembly: ArturRios.Util.WebApi
File(s): /home/ci/agent/workspace/dotnet-webapi-util-docs_main/app/src/Middleware/ExceptionMiddleware.cs
Line coverage
85%
Covered lines: 30
Uncovered lines: 5
Coverable lines: 35
Total lines: 95
Line coverage: 85.7%
Branch coverage
75%
Covered branches: 9
Total branches: 12
Branch coverage: 75%
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()75%121292.3%

File(s)

/home/ci/agent/workspace/dotnet-webapi-util-docs_main/app/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.AspNetCore.WebUtilities;
 7using Microsoft.Extensions.Logging;
 8
 9namespace ArturRios.Util.WebApi.Middleware;
 10
 11/// <summary>
 12/// Catches unhandled exceptions raised further down the pipeline and converts them into a JSON error response,
 13/// logging the exception and quietly ignoring client-initiated request cancellations. An
 14/// <see cref="EndpointDisabledException"/> is answered with its own status code rather than 500, and so is a
 15/// <see cref="BadHttpRequestException"/> (a client fault such as a request body over the size limit), with its
 16/// status's reason phrase as the error. When the response
 17/// has already started, the exception is rethrown so the server aborts the response instead of leaving the client
 18/// with a truncated body that looks complete.
 19/// </summary>
 20/// <param name="next">The next middleware in the pipeline.</param>
 21/// <param name="logger">Used to log unhandled exceptions and cancellations.</param>
 4622public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger) : WebApiMiddleware
 23{
 24    private const string InternalServerError = "Internal server error, please try again later";
 25    private const string BadRequestMessage = "Bad Request";
 26
 27    /// <summary>Invokes the next middleware, catching any unhandled exception and writing an error response instead of 
 28    /// <param name="httpContext">The current HTTP context.</param>
 29    public async Task InvokeAsync(HttpContext httpContext)
 30    {
 31        try
 32        {
 4133            await next(httpContext);
 2934        }
 035        catch (OperationCanceledException oce) when (httpContext.RequestAborted.IsCancellationRequested)
 36        {
 37            // TaskCanceledException derives from OperationCanceledException, so this covers both.
 038            logger.LogDebug("Request was canceled by the client: {Message}", oce.Message);
 039        }
 1240        catch (Exception ex) when (!httpContext.Response.HasStarted)
 41        {
 1142            await HandleException(httpContext, ex);
 43        }
 4044    }
 45
 46    private async Task HandleException(HttpContext context, Exception exception)
 47    {
 1148        if (context.RequestAborted.IsCancellationRequested)
 49        {
 050            logger.LogDebug(exception, "Cannot write an error response because the request was aborted.");
 51
 052            return;
 53        }
 54
 55        int statusCode;
 56        string[] errors;
 57
 58        switch (exception)
 59        {
 60            case EndpointDisabledException disabled:
 161                logger.LogInformation("Request reached a disabled endpoint: {Path}", context.Request.Path);
 162                statusCode = disabled.StatusCode;
 163                errors = disabled.Messages;
 164                break;
 65            case BadHttpRequestException badRequest:
 66                // A client fault Kestrel or a binder detected (e.g. a body over MaxRequestBodySize is a 413);
 67                // Kestrel answers with its StatusCode when nothing catches it, so this must not become a 500.
 268                logger.LogInformation("Rejected a bad request with {StatusCode}: {Message}", badRequest.StatusCode,
 269                    badRequest.Message);
 270                statusCode = badRequest.StatusCode;
 271                errors = [ReasonPhrases.GetReasonPhrase(badRequest.StatusCode) is { Length: > 0 } reason
 272                    ? reason
 273                    : BadRequestMessage];
 274                break;
 75            case CustomException custom:
 176                logger.LogError(exception, "Unhandled exception while processing the request.");
 177                statusCode = HttpStatusCodes.InternalServerError;
 178                errors = custom.Messages;
 179                break;
 80            default:
 781                logger.LogError(exception, "Unhandled exception while processing the request.");
 782                statusCode = HttpStatusCodes.InternalServerError;
 783                errors = [InternalServerError];
 84                break;
 85        }
 86
 87        // Errors, not messages: Success is computed from Errors, so a 500 whose text sat under Messages
 88        // came back with "success": true. AuthenticationMiddleware already envelopes its 401 this way.
 1189        var output = DataOutput<string>.New
 1190            .WithData(string.Empty)
 1191            .WithErrors(errors);
 92
 1193        await context.Response.WriteOutputAsync(statusCode, output);
 1194    }
 95}