| | | 1 | | /******************************************************************************** |
| | | 2 | | * RouterBuilderExceptionExtensions.cs * |
| | | 3 | | * * |
| | | 4 | | * Author: Denes Solti * |
| | | 5 | | ********************************************************************************/ |
| | | 6 | | using System; |
| | | 7 | | using System.Diagnostics.CodeAnalysis; |
| | | 8 | | using System.Collections.Generic; |
| | | 9 | | using System.Linq; |
| | | 10 | | using System.Net; |
| | | 11 | | using System.Net.Http; |
| | | 12 | | |
| | | 13 | | namespace 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> |
| | 1 | 35 | | 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) |
| | 1 | 52 | | { |
| | 1 | 53 | | Ensure.NotNull(routeBuilder); |
| | 1 | 54 | | Ensure.NotNull(pattern); |
| | | 55 | | |
| | 1 | 56 | | 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) |
| | 1 | 75 | | { |
| | 1 | 76 | | Ensure.NotNull(routeBuilder); |
| | 1 | 77 | | Ensure.NotNull(verbs); |
| | 1 | 78 | | Ensure.NotNull(pattern); |
| | | 79 | | |
| | 1 | 80 | | routeBuilder.AddHandler(verbs, pattern, async (RequestContext context, CallNextHandlerDelegate next) => |
| | 1 | 81 | | { |
| | 1 | 82 | | try |
| | 1 | 83 | | { |
| | 1 | 84 | | return await next(); |
| | 1 | 85 | | } |
| | 1 | 86 | | catch (Exception ex) when (ex is not (HttpRequestException or OperationCanceledException /*needs to |
| | 1 | 87 | | { |
| | 1 | 88 | | switch (ex) |
| | 1 | 89 | | { |
| | 1 | 90 | | case AggregateException aggregateException: |
| | 1 | 91 | | HttpRequestException.Throw(HttpStatusCode.InternalServerError, Resources.ERR_INTERNAL_ER |
| | 1 | 92 | | break; |
| | 1 | 93 | | } |
| | 1 | 94 | | |
| | 1 | 95 | | HttpRequestException.Throw(HttpStatusCode.InternalServerError, Resources.ERR_INTERNAL_ERROR, ex, |
| | 1 | 96 | | return null!; |
| | 1 | 97 | | } |
| | 1 | 98 | | }); |
| | | 99 | | |
| | 1 | 100 | | 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) |
| | 1 | 119 | | { |
| | 1 | 120 | | Ensure.NotNull(routeBuilder); |
| | 1 | 121 | | Ensure.NotNull(verb); |
| | 1 | 122 | | Ensure.NotNull(pattern); |
| | | 123 | | |
| | 1 | 124 | | 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) |
| | 1 | 139 | | { |
| | 1 | 140 | | Ensure.NotNull(routeBuilder); |
| | 1 | 141 | | Ensure.NotNull(verbs); |
| | | 142 | | |
| | 1 | 143 | | 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] |
| | 1 | 183 | | 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 |
| | 1 | 195 | | { |
| | 1 | 196 | | HttpRequestException ex = new(title, original); |
| | | 197 | | |
| | 1 | 198 | | ex.Data[StatusName] = status; |
| | | 199 | | |
| | 1 | 200 | | if (errors?.ToArray() is { Length: > 0 } err) |
| | 1 | 201 | | ex.Data[ErrorsName] = err; // On .NET FW the Data members must be serializable (string[] it is) |
| | | 202 | | |
| | 1 | 203 | | if (developerMessages?.ToArray() is { Length: > 0 } dev) |
| | 1 | 204 | | ex.Data[DeveloperMessagesName] = dev; |
| | | 205 | | |
| | 1 | 206 | | 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) |
| | 1 | 221 | | { |
| | 1 | 222 | | Ensure.NotNull(requestException); |
| | | 223 | | |
| | 1 | 224 | | return new ErrorDetails |
| | 1 | 225 | | { |
| | 1 | 226 | | Status = requestException.Data[StatusName] switch |
| | 1 | 227 | | { |
| | 1 | 228 | | HttpStatusCode status => status, |
| | 1 | 229 | | int intStatus => (HttpStatusCode) intStatus, |
| | 1 | 230 | | _ => HttpStatusCode.InternalServerError |
| | 1 | 231 | | }, |
| | 1 | 232 | | Title = requestException.Message, |
| | 1 | 233 | | TraceId = traceId ?? Guid.NewGuid().ToString("N"), |
| | 1 | 234 | | Errors = requestException.Data[ErrorsName] as IEnumerable<string>, |
| | 1 | 235 | | DeveloperMessages = populateErrorInfo ? requestException.Data[DeveloperMessagesName] as IEnumerable< |
| | 1 | 236 | | }; |
| | | 237 | | } |
| | | 238 | | } |
| | | 239 | | } |
| | | 240 | | } |