Skip to content

fix: implement ASP.NET Core 10 error handling#685

Merged
KrzysztofPajak merged 6 commits intodevelopfrom
copilot/check-error-handling-implementation
May 3, 2026
Merged

fix: implement ASP.NET Core 10 error handling#685
KrzysztofPajak merged 6 commits intodevelopfrom
copilot/check-error-handling-implementation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 29, 2026

Fixes several error handling defects identified during an ASP.NET Core 10 compliance review.

Changes

1. New GrandExceptionHandler — implements IExceptionHandler (ASP.NET Core 8+ pattern)

src/Web/Grand.Web.Common/Infrastructure/GrandExceptionHandler.cs (new file)

  • Registered with services.AddExceptionHandler<GrandExceptionHandler>().
  • Guards with HasStarted before touching the response.
  • Logs the exception via structured logging (constant template, not exception.Message).
  • API requests: writes an RFC 7807 application/problem+json response (status 500) via IProblemDetailsService and returns true — handled.
  • Web (Razor/MVC) requests: redirects to /errorpage.htm (302 → /errorpage.htm, permanent: false) and returns true — always handled, never falls through to unreliable path-based re-execution.

2. ErrorHandlerStartup.ConfigureServices — register new services

services.AddProblemDetails();
services.AddExceptionHandler<GrandExceptionHandler>();

3. Fix dual UseExceptionHandler registration bug + reliable MVC Razor error page

Before (broken):

application.UseExceptionHandler("/errorpage.htm");   // ← first registration
// ... then immediately:
application.UseExceptionHandler(handler => { ... }); // ← second registration silently wins
                                                      //   production NEVER reached /errorpage.htm

After:

// GrandExceptionHandler handles all requests (API → ProblemDetails, web → redirect to /errorpage.htm)
// No path fallback: path-based re-execution is unreliable in the MVC Razor pipeline because
// UseGrandExceptionHandler runs at priority -10 while UseGrandStaticFiles runs at priority 100.
application.UseExceptionHandler();

The path-based fallback (UseExceptionHandler("/errorpage.htm")) was removed because it is unreliable in an ASP.NET MVC Razor pipeline: if the re-executed path returned 404 for any reason (routing intercepting before static files, or the file absent in non-Grand.Web apps), ExceptionHandlerMiddleware detected the 404 and re-threw the original exception — showing HTTP 404 to the user instead of the error page.

4. Fix UsePageNotFound302 Redirect → re-execute with 404-only restriction

Before: Response.Redirect(...) returned 302 → GET /page-not-found → 200. SEO and clients saw the wrong status.

After: UseStatusCodePagesWithReExecute("/page-not-found") re-executes the pipeline while keeping the original 404 status code. An inline middleware:

  • Disables the feature entirely for API (Bearer) and static-resource requests.
  • After the inner pipeline runs, also disables the feature for any non-404 response (e.g. 401, 403, 405) so those status codes are not mistakenly routed to /page-not-found in ASP.NET MVC Razor applications.

FileExtensionContentTypeProvider is reused as a private static readonly field (was previously allocated per request), and the static-file check is extracted into an IsStaticFileRequest named helper.

5. Fix ApiAuthenticationRegistrar — stack-trace loss + plain-text 401

Before:

catch (Exception ex) { throw new Exception(ex.Message); }  // discards stack trace

OnAuthenticationFailed = async context =>
{
    context.Response.StatusCode = 401;
    context.Response.ContentType = "text/plain";
    await context.Response.WriteAsync(context.Exception.Message);  // leaks internal message
};

After:

// try/catch removed entirely — exceptions propagate naturally, preserving stack trace

OnAuthenticationFailed = async context =>
{
    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
    if (problemDetailsService != null)
        await problemDetailsService.WriteAsync(...);  // RFC 7807 via IProblemDetailsService
    else
    {
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(new ProblemDetails { Status = 401, Title = "Authentication failed" });
    }
    // Fallback ensures API clients always receive a usable 401 body regardless of DI registration
};

6. Fix BaseController.LogException — structured logging template

// Before — exception.Message used as a format template (breaks on { } in messages)
logger.LogError(exception, exception.Message);

// After
logger.LogError(exception, "An error occurred");

7. Eliminate duplicate Authorization-header check

IsApiRequest(HttpRequest) extracted as an internal static helper on ApplicationBuilderExtensions. UsePageNotFound, UseBadRequestResult, and GrandExceptionHandler all call it instead of duplicating the Bearer-scheme check logic.

Testing

  • Grand.Web.Common.Tests: 11/11 passed
  • Build: 0 errors, 0 new warnings

Copilot AI and others added 2 commits April 29, 2026 18:59
- 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>
Comment thread src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs Outdated
…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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 30, 2026

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:

  • ccsca2021.crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • ccsca2021.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • subca.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from KrzysztofPajak April 30, 2026 18:27
@KrzysztofPajak KrzysztofPajak marked this pull request as ready for review April 30, 2026 18:34
Copilot AI review requested due to automatic review settings April 30, 2026 18:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() and AddExceptionHandler<GrandExceptionHandler>() and introduces GrandExceptionHandler to produce RFC7807 responses for API requests.
  • Refactors API-request detection into ApplicationBuilderExtensions.IsApiRequest(HttpRequest) and reworks 404 handling to use UseStatusCodePagesWithReExecute.
  • Updates API JWT auth failure handling to return ProblemDetails and 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.

Comment thread src/Web/Grand.Web.Common/Infrastructure/ApplicationBuilderExtensions.cs Outdated
@KrzysztofPajak
Copy link
Copy Markdown
Member

@copilot please check that your proposition is correct for the ASP.NET MVC razor project

…tentTypeProvider; remove redundant try/catch; add OnAuthenticationFailed fallback

Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/24b98e75-69d0-4ef3-a43e-2feb96bb1479

Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 2, 2026

@copilot please check that your proposition is correct for the ASP.NET MVC razor project

Verified and fixed in 2e3490f. The original UseStatusCodePagesWithReExecute("/page-not-found") was indeed incorrect for an ASP.NET MVC Razor project — it would re-execute the pipeline for all 4xx/5xx responses with an empty body, not just 404. That means a Razor action returning Unauthorized() (401) or Forbid() with an empty body would end up at /page-not-found instead of being handled appropriately.

The fix adds a post-next check in the inline middleware: after the inner pipeline runs, if the response status is not 404, the IStatusCodePagesFeature is disabled. This ensures:

  • 404 → re-executes at /page-not-found (preserving the 404 status) ✅
  • 400 → handled exclusively by UseBadRequestResult
  • 401, 403, 405, 500, etc. → pass through unchanged ✅
  • API (Bearer) and static-resource requests → feature disabled upfront (unchanged) ✅

Also addressed in the same commit:

  • FileExtensionContentTypeProvider extracted as a static field (was allocated per request)
  • Extracted IsStaticFileRequest named helper for clarity
  • Removed redundant try/catch (Exception) { throw; } blocks from ApiAuthenticationRegistrar
  • Added application/problem+json fallback in OnAuthenticationFailed for when IProblemDetailsService is not registered
  • Fixed "API is disable" → "API is disabled" typo

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:

  • ccsca2021.crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • ccsca2021.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)
  • subca.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj --no-incremental -v q (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@KrzysztofPajak
Copy link
Copy Markdown
Member

@copilot UseExceptionHandler never execute event if for testing I've add exception in CatalogController - it shows HTTP ERROR 404

…move path fallback from UseExceptionHandler

Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/5190d235-2337-4c71-add0-49709d3379b1

Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 2, 2026

@copilot UseExceptionHandler never execute event if for testing I've add exception in CatalogController - it shows HTTP ERROR 404

Fixed in 6b56346.

The root cause: GrandExceptionHandler.TryHandleAsync was returning false for web/Razor requests, causing ExceptionHandlerMiddleware to fall back to re-executing the pipeline at the static path /errorpage.htm. In the GrandNode MVC Razor pipeline (where UseGrandExceptionHandler runs at priority -10, well before UseGrandStaticFiles at priority 100), if that path-based re-execution ever resulted in a 404 (routing intercepting before static files, or the file absent in a non-Grand.Web app), ExceptionHandlerMiddleware detected the 404 from the error path and re-threw the original exception — leaving the user with HTTP 404 instead of the error page.

Two changes in 6b56346:

  1. GrandExceptionHandler.TryHandleAsync — for non-API (web/Razor) requests, instead of return false, it now calls Response.Redirect("/errorpage.htm", permanent: false) and returns true. The exception is fully handled; no path-based re-execution needed.
  2. UseGrandExceptionHandler — changed UseExceptionHandler("/errorpage.htm") to UseExceptionHandler() (no path). Since GrandExceptionHandler now always returns true (API → ProblemDetails, web → redirect to error page), the unreliable path fallback is no longer registered.

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:

  • ccsca2021.crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj -c Release (dns block)
  • ccsca2021.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj -c Release (dns block)
  • crl.certum.pl
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj -c Release (dns block)
  • subca.ocsp-certum.com
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Web/Grand.Web.Common/Grand.Web.Common.csproj -c Release (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from KrzysztofPajak May 2, 2026 17:42
Changed "DisplayFullErrorStack" in appsettings.json from true to false to prevent full error stack traces from being shown in production environments. This enhances security and user experience by limiting error details exposed to end users.
@KrzysztofPajak KrzysztofPajak changed the title fix: implement ASP.NET Core 10 error handling best practices fix: implement ASP.NET Core 10 error handling May 3, 2026
@KrzysztofPajak KrzysztofPajak merged commit 2abd369 into develop May 3, 2026
6 checks passed
@KrzysztofPajak KrzysztofPajak deleted the copilot/check-error-handling-implementation branch May 3, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants