fix: implement ASP.NET Core 10 error handling best practices#685
fix: implement ASP.NET Core 10 error handling best practices#685
Conversation
- Add GrandExceptionHandler implementing IExceptionHandler (ASP.NET Core 8+ pattern):
logs exceptions, returns RFC 7807 ProblemDetails JSON for API (Bearer) requests,
returns false for web requests so the configured error page handles the response.
Guards with HasStarted before writing.
- Register AddExceptionHandler<GrandExceptionHandler>() and AddProblemDetails() in
ErrorHandlerStartup.ConfigureServices.
- Fix UseGrandExceptionHandler: remove dual UseExceptionHandler registration bug.
Single UseExceptionHandler('/errorpage.htm') now invokes the IExceptionHandler chain
first, then falls back to re-executing at /errorpage.htm for non-API requests.
- Fix UsePageNotFound: replace 302 Redirect with UseStatusCodePagesWithReExecute
('/page-not-found') which preserves the original 404 status code. Add inline
middleware to disable status code pages for API (Bearer) and static-resource requests
so those callers receive their original response unchanged.
- Fix ApiAuthenticationRegistrar: replace catch(Exception ex) { throw new Exception(ex.Message) }
with bare throw to preserve the original stack trace; update OnAuthenticationFailed to
return application/problem+json via IProblemDetailsService instead of plain text.
- Fix BaseController.LogException: use a constant message template instead of
exception.Message to avoid structured-logging format issues.
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/ecc051c4-72f1-4d90-8ec9-025f19ab46c3
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
… Split()[0])" Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/ecc051c4-72f1-4d90-8ec9-025f19ab46c3 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
…der check Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/1ec7db4a-11fc-49b4-8aa2-85f12990ec16 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
Pull request overview
Updates GrandNode’s ASP.NET Core error-handling pipeline to use the IExceptionHandler + ProblemDetails pattern, aiming for consistent API error responses while preserving existing HTML error pages for browser requests.
Changes:
- Registers
AddProblemDetails()andAddExceptionHandler<GrandExceptionHandler>()and introducesGrandExceptionHandlerto produce RFC7807 responses for API requests. - Refactors API-request detection into
ApplicationBuilderExtensions.IsApiRequest(HttpRequest)and reworks 404 handling to useUseStatusCodePagesWithReExecute. - Updates API JWT auth failure handling to return
ProblemDetailsand preserves stack traces on token-validation failures.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Web/Grand.Web.Common/Startup/ErrorHandlerStartup.cs | Registers ProblemDetails + exception handler services for the new error pipeline. |
| src/Web/Grand.Web.Common/Infrastructure/GrandExceptionHandler.cs | Adds IExceptionHandler implementation to log and emit RFC7807 for API errors. |
| src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs | Adds IsApiRequest helper, removes duplicate exception-handler registration, and changes 404 handling behavior. |
| src/Web/Grand.Web.Common/Controllers/BaseController.cs | Adjusts error logging template for consistency. |
| src/Modules/Grand.Module.Api/Infrastructure/ApiAuthenticationRegistrar.cs | Returns ProblemDetails on auth failures and preserves exception stack traces. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -52,9 +60,9 @@ public void AddAuthentication(AuthenticationBuilder builder, IConfiguration conf | |||
| throw new Exception("API is disable"); | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| catch (Exception) | |||
| { | |||
| throw new Exception(ex.Message); | |||
| throw; | |||
| } | |||
There was a problem hiding this comment.
The try/catch (Exception) { throw; } block is functionally redundant and adds noise. Since the goal is preserving the stack trace, you can remove the try/catch entirely and let exceptions propagate naturally.
| // UseStatusCodePagesWithReExecute sets IStatusCodePagesFeature.Enabled = true and re-executes | ||
| // the pipeline at the specified path when a 404 occurs, preserving the original 404 status code. | ||
| application.UseStatusCodePagesWithReExecute("/page-not-found"); | ||
|
|
||
| // Disable status code pages for API (Bearer) requests and static resource requests so that | ||
| // those callers receive the original response rather than the HTML not-found page. | ||
| application.Use(async (context, next) => |
There was a problem hiding this comment.
UseStatusCodePagesWithReExecute("/page-not-found") will re-execute for all 4xx/5xx responses with an empty body, not just 404. This can accidentally route 400/401/403/etc through the not-found endpoint (and also interferes with the earlier UseBadRequestResult() middleware). Consider disabling status-code pages unless Response.StatusCode == 404 (e.g., by toggling IStatusCodePagesFeature.Enabled after next runs) or using a path format that includes the status code and mapping 404 separately.
| var contentTypeProvider = new FileExtensionContentTypeProvider(); | ||
| var staticResource = contentTypeProvider.TryGetContentType(context.HttpContext.Request.Path, out _); | ||
|
|
||
| if (!apiRequest && !staticResource) | ||
| if (contentTypeProvider.TryGetContentType(context.Request.Path, out _)) | ||
| { | ||
| const string location = "/page-not-found"; | ||
| context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location); | ||
| var feature = context.Features.Get<IStatusCodePagesFeature>(); | ||
| if (feature != null) | ||
| feature.Enabled = false; |
There was a problem hiding this comment.
FileExtensionContentTypeProvider is instantiated on every non-API request here. Since this middleware runs for all requests, this becomes a per-request allocation; consider reusing a static/singleton FileExtensionContentTypeProvider (it’s stateless) or another cheap static-resource check to avoid unnecessary allocations.
| context.Response.StatusCode = StatusCodes.Status401Unauthorized; | ||
| var problemDetailsService = context.HttpContext.RequestServices.GetService<IProblemDetailsService>(); | ||
| if (problemDetailsService != null) | ||
| await problemDetailsService.WriteAsync(new ProblemDetailsContext { | ||
| HttpContext = context.HttpContext, | ||
| ProblemDetails = new ProblemDetails { | ||
| Status = StatusCodes.Status401Unauthorized, | ||
| Title = "Authentication failed" | ||
| } | ||
| }); | ||
| }, |
There was a problem hiding this comment.
OnAuthenticationFailed now writes a response body only when IProblemDetailsService is registered; if it isn’t, the handler returns with a 401 and an empty body/content-type. Add a fallback (e.g., set ContentType and write a minimal RFC7807 JSON payload) so API clients consistently receive a usable 401 response.
GrandExceptionHandlerimplementingIExceptionHandler(ASP.NET Core 8+ pattern)AddExceptionHandler<GrandExceptionHandler>()andAddProblemDetails()inErrorHandlerStartup.ConfigureServicesUseGrandExceptionHandler: remove dualUseExceptionHandlerregistration bugUsePageNotFound: replace302 RedirectwithUseStatusCodePagesWithReExecuteApiAuthenticationRegistrar: preserve stack trace; returnProblemDetailson 401BaseController.LogException: use constant log message templateauthHeader.Split(' ')[0]withStartsWithIsApiRequest(HttpRequest)helper inApplicationBuilderExtensions(internal) — eliminates duplicate Authorization-header check logic acrossUsePageNotFound,UseBadRequestResult, andGrandExceptionHandler