< Summary

Information
Class: NanoRoute.NanoRouteExceptionExtensions
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/NanoRouteExceptionExtensions.cs
Line coverage
100%
Covered lines: 62
Uncovered lines: 0
Coverable lines: 62
Total lines: 240
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/NanoRouteExceptionExtensions.cs

#LineLine coverage
 1/********************************************************************************
 2* RouterBuilderExceptionExtensions.cs                                           *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Collections.Generic;
 9using System.Linq;
 10using System.Net;
 11using System.Net.Http;
 12
 13namespace NanoRoute
 14{
 15    using Internals;
 16    using Properties;
 17
 18    /// <summary>
 19    /// Adds helpers for normalizing exceptions and extracting structured error details.
 20    /// </summary>
 21    public static class NanoRouteExceptionExtensions
 22    {
 23        extension<TBuilder>(TBuilder routeBuilder) where TBuilder : RouteBuilder
 24        {
 25            /// <summary>
 26            /// Adds an exception-handling middleware for all supported HTTP methods.
 27            /// </summary>
 28            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 29            /// <remarks>
 30            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 31            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 32            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 33            /// not normalized so caller-driven cancellation can propagate unchanged.
 34            /// </remarks>
 135            public TBuilder AddExceptionHandler() => routeBuilder.AddExceptionHandler(Enum.GetNames(typeof(HttpVerb)));
 36
 37            /// <summary>
 38            /// Adds an exception-handling middleware for all supported HTTP methods.
 39            /// </summary>
 40            /// <param name="pattern">
 41            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 42            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 43            /// </param>
 44            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 45            /// <remarks>
 46            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 47            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 48            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 49            /// not normalized so caller-driven cancellation can propagate unchanged.
 50            /// </remarks>
 51            public TBuilder AddExceptionHandler(string pattern)
 152            {
 153                Ensure.NotNull(routeBuilder);
 154                Ensure.NotNull(pattern);
 55
 156                return routeBuilder.AddExceptionHandler(Enum.GetNames(typeof(HttpVerb)), pattern);
 57            }
 58
 59            /// <summary>
 60            /// Adds an exception-handling middleware for the selected HTTP methods.
 61            /// </summary>
 62            /// <param name="verbs">The HTTP methods that should use the exception-handling middleware.</param>
 63            /// <param name="pattern">
 64            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 65            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 66            /// </param>
 67            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 68            /// <remarks>
 69            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 70            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 71            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 72            /// not normalized so caller-driven cancellation can propagate unchanged.
 73            /// </remarks>
 74            public TBuilder AddExceptionHandler(IEnumerable<string> verbs, string pattern)
 175            {
 176                Ensure.NotNull(routeBuilder);
 177                Ensure.NotNull(verbs);
 178                Ensure.NotNull(pattern);
 79
 180                routeBuilder.AddHandler(verbs, pattern, async (RequestContext context, CallNextHandlerDelegate next) =>
 181                {
 182                    try
 183                    {
 184                        return await next();
 185                    }
 186                    catch (Exception ex) when (ex is not (HttpRequestException or OperationCanceledException /*needs to 
 187                    {
 188                        switch (ex)
 189                        {
 190                            case AggregateException aggregateException:
 191                                HttpRequestException.Throw(HttpStatusCode.InternalServerError, Resources.ERR_INTERNAL_ER
 192                                break;
 193                        }
 194
 195                        HttpRequestException.Throw(HttpStatusCode.InternalServerError, Resources.ERR_INTERNAL_ERROR, ex,
 196                        return null!;
 197                    }
 198                });
 99
 1100                return routeBuilder;
 101            }
 102
 103            /// <summary>
 104            /// Adds an exception-handling middleware for a single HTTP method.
 105            /// </summary>
 106            /// <param name="verb">The HTTP method that should use the exception-handling middleware.</param>
 107            /// <param name="pattern">
 108            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 109            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 110            /// </param>
 111            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 112            /// <remarks>
 113            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 114            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 115            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 116            /// not normalized so caller-driven cancellation can propagate unchanged.
 117            /// </remarks>
 118            public TBuilder AddExceptionHandler(string verb, string pattern)
 1119            {
 1120                Ensure.NotNull(routeBuilder);
 1121                Ensure.NotNull(verb);
 1122                Ensure.NotNull(pattern);
 123
 1124                return routeBuilder.AddExceptionHandler([verb], pattern);
 125            }
 126
 127            /// <summary>
 128            /// Adds an exception-handling middleware for the selected HTTP methods.
 129            /// </summary>
 130            /// <param name="verbs">The HTTP methods that should use the exception-handling middleware.</param>
 131            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 132            /// <remarks>
 133            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 134            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 135            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 136            /// not normalized so caller-driven cancellation can propagate unchanged.
 137            /// </remarks>
 138            public TBuilder AddExceptionHandler(IEnumerable<string> verbs)
 1139            {
 1140                Ensure.NotNull(routeBuilder);
 1141                Ensure.NotNull(verbs);
 142
 1143                return routeBuilder.AddExceptionHandler(verbs, "/");
 144            }
 145        }
 146
 147        /// <summary>
 148        /// The <see cref="Exception.Data"/> key used to store client-facing error messages.
 149        /// </summary>
 150        /// <remarks>
 151        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 152        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 153        /// </remarks>
 154        public const string ErrorsName = "Errors";
 155
 156        /// <summary>
 157        /// The <see cref="Exception.Data"/> key used to store developer-facing diagnostic details.
 158        /// </summary>
 159        /// <remarks>
 160        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 161        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 162        /// </remarks>
 163        public const string DeveloperMessagesName = "DeveloperMessages";
 164
 165        /// <summary>
 166        /// The <see cref="Exception.Data"/> key used to store the HTTP status code.
 167        /// </summary>
 168        /// <remarks>
 169        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 170        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 171        /// </remarks>
 172        public const string StatusName = "StatusCode";
 173
 174        extension(HttpRequestException)
 175        {
 176            /// <summary>
 177            /// Throws an <see cref="HttpRequestException"/> enriched with an HTTP status code and public error messages
 178            /// </summary>
 179            /// <param name="status">The HTTP status code that should be associated with the exception.</param>
 180            /// <param name="title">The human-readable error title.</param>
 181            /// <param name="errors">Optional client-facing error messages that should not contain sensitive data.</para
 182            [DoesNotReturn]
 1183            public static void Throw(HttpStatusCode status, string title, params IEnumerable<string> errors) => Throw(st
 184
 185            /// <summary>
 186            /// Throws an <see cref="HttpRequestException"/> enriched with routing-specific metadata.
 187            /// </summary>
 188            /// <param name="status">The HTTP status code that should be associated with the exception.</param>
 189            /// <param name="title">The human-readable error title.</param>
 190            /// <param name="original">The original exception, if any.</param>
 191            /// <param name="errors">Optional client-facing error messages that should not contain sensitive data.</para
 192            /// <param name="developerMessages">Optional developer-facing messages that may contain sensitive data.</par
 193            [DoesNotReturn]
 194            public static void Throw(HttpStatusCode status, string title, Exception? original = null, IEnumerable<string
 1195            {
 1196                HttpRequestException ex = new(title, original);
 197
 1198                ex.Data[StatusName] = status;
 199
 1200                if (errors?.ToArray() is { Length: > 0 } err)
 1201                    ex.Data[ErrorsName] = err;  // On .NET FW the Data members must be serializable (string[] it is)
 202
 1203                if (developerMessages?.ToArray() is { Length: > 0 } dev)
 1204                    ex.Data[DeveloperMessagesName] = dev;
 205
 1206                throw ex;
 207            }
 208        }
 209
 210        extension(HttpRequestException requestException)
 211        {
 212            /// <summary>
 213            /// Converts an <see cref="HttpRequestException"/> into an <see cref="ErrorDetails"/> payload.
 214            /// </summary>
 215            /// <param name="populateErrorInfo">
 216            /// <see langword="true"/> to include developer-facing details when present; otherwise <see langword="false"
 217            /// </param>
 218            /// <param name="traceId">The trace identifier to expose in the resulting payload.</param>
 219            /// <returns>The structured error payload.</returns>
 220            public ErrorDetails GetErrorDetails(bool populateErrorInfo = false, string? traceId = null)
 1221            {
 1222                Ensure.NotNull(requestException);
 223
 1224                return new ErrorDetails
 1225                {
 1226                    Status = requestException.Data[StatusName] switch
 1227                    {
 1228                        HttpStatusCode status => status,
 1229                        int intStatus => (HttpStatusCode) intStatus,
 1230                        _ => HttpStatusCode.InternalServerError
 1231                    },
 1232                    Title = requestException.Message,
 1233                    TraceId = traceId ?? Guid.NewGuid().ToString("N"),
 1234                    Errors = requestException.Data[ErrorsName] as IEnumerable<string>,
 1235                    DeveloperMessages = populateErrorInfo ? requestException.Data[DeveloperMessagesName] as IEnumerable<
 1236                };
 237            }
 238        }
 239    }
 240}