Skip to content

Error Pages

SimpleModule ships a consistent error-handling pipeline that serves the right response shape depending on who's asking:

  • Inertia (browser) requests get a React error page rendered as a full Inertia response.
  • API / fetch requests get an RFC 7807 ProblemDetails JSON body.
  • Catastrophic failures (exception before Inertia can render) fall back to a static wwwroot/error.html.

No per-module wiring is required — everything below is turned on by AddSimpleModuleInfrastructure() and UseSimpleModule().

How Dispatch Works

Thrown exceptions

The framework registers GlobalExceptionHandler via AddExceptionHandler<GlobalExceptionHandler>(). It maps domain exceptions to HTTP status codes:

ExceptionStatusNotes
ValidationException400Errors serialized into the errors extension
ArgumentException400Fallback for unchecked arg errors
NotFoundException404Prefer this over returning NotFound() for domain misses
ForbiddenException403For authorization failures surfaced from services
ConflictException409Optimistic concurrency, duplicate keys, etc.
anything else500Logged at Error; the response message is ErrorMessages.UnexpectedError

The handler inspects X-Inertia on the request:

  • Present → writes an Inertia page JSON with component Error/{statusCode} and props { status, title, message }.
  • Absent → writes ProblemDetails JSON.

Unmatched routes

UseSimpleModule() registers a MapFallback for GET requests so browser navigation to a non-existent URL gets a 404 page instead of a bare 404:

text
GET /some/unknown/path
  → MapFallback
  → RenderErrorPage(404)
  → Inertia.Render("Error/404", { status, title, message })

The fallback only fires for unmatched requests. Endpoints that return bare 401/403 from authentication middleware are untouched, so API tests that assert on those status codes keep working.

Direct error URLs

GET /error/{statusCode} renders an Inertia error page for any status — useful for linking from emails, redirects, or testing.

text
GET /error/403
  → Inertia.Render("Error/403", { status: 403, title: "...", message: "..." })

Static fallback

If an exception occurs so early that Inertia can't render (e.g., DI resolution failure), UseExceptionHandler writes the contents of wwwroot/error.html with a 500 status. Keep this file lean — it must render without any server state.

Raising Domain Errors

Throw the framework exceptions from services or endpoints; the handler takes care of the status code and response shape.

csharp
public async Task<Customer> GetCustomerAsync(CustomerId id)
{
    var customer = await db.Customers.FindAsync(id);
    if (customer is null)
    {
        throw new NotFoundException("Customer", id);
    }
    return customer;
}
csharp
public async Task DeactivateCustomerAsync(CustomerId id, UserId actor)
{
    var customer = await db.Customers.FindAsync(id);
    if (customer is null)
    {
        throw new NotFoundException("Customer", id);
    }
    if (customer.OwnerId != actor)
    {
        throw new ForbiddenException("You cannot deactivate another tenant's customer.");
    }
    // ...
}

React Error Components

template/SimpleModule.Host/ClientApp/app.tsx maps the Error/* component names to React components before any normal page resolution runs:

tsx
const ERROR_PAGES: Record<string, { default: React.ComponentType }> = {
  'Error/404': { default: ErrorPage404 },
  'Error/403': { default: ErrorPage403 },
  'Error/500': { default: ErrorPage500 },
};

createInertiaApp({
  resolve: async (name) => {
    if (name in ERROR_PAGES) {
      return ERROR_PAGES[name];
    }
    // ...normal page resolution
  },
});

The default components come from @simplemodule/ui. To customize, swap in your own component for the relevant status code. The component receives { status, title, message } via Inertia props.

Customizing

  • Change messages — override ErrorMessages constants, or pass custom title/message via a new exception type.
  • Add a status code — create a new exception mapping in GlobalExceptionHandler and a matching Error/{code} React component in ERROR_PAGES.
  • Static HTML fallback — edit template/SimpleModule.Host/wwwroot/error.html. Keep it self-contained (inlined CSS, no external assets) so it works when the pipeline is degraded.

Testing Error Pages

Assert on the status code and, for Inertia flows, the component name:

csharp
[Fact]
public async Task Missing_customer_returns_404_problem_details()
{
    using var client = factory.CreateAuthenticatedClient();

    var response = await client.GetAsync("/api/customers/99999");

    response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
    problem!.Title.Should().Be("Resource not found");
}

For Inertia pages, send X-Inertia: true and assert on the JSON component field.

Next Steps

  • Endpoints — how validation errors become 400 responses
  • Permissions — how RequirePermission interacts with 403 responses

Released under the MIT License.