Skip to content

A page can only answer 200, 404 or 500 #784

Open

Description

@karthickeyan-dev

Version

void 1.7.0, and main at f876b9d

Problem

A page can't answer with any status but 200, 404 or 500.

renderedResponse() and pageJsonResponse() in src/pages/protocol.ts build the page response with headers only, so a status the loader sets is dropped:

// pages/banned.server.ts
export const loader = defineHandler((c) => {
  c.status(403);
  return {};
});

This page answers 200, on full loads and on client visits.

The new _not-found and _error boundaries do render with a status, but renderPageFailure() picks only 404 or 500. Any other HTTPException, such as new HTTPException(503, { message: 'Down for maintenance' }), skips _error and goes out as a bare text response.

So an app can't render a React page for:

  • 403, such as a "your account is banned" page. It shows "403" but answers 200, so monitoring and crawlers see a normal page.
  • 503, such as a maintenance page served from middleware with Retry-After. Search engines treat a 200 as the page's new content. A 503 tells them to come back later. Today the only way to send a 503 is hand-written HTML returned from middleware, which can't use the app's layouts or bundled CSS.
  • 401, 410 and 429, which have the same problem.

Proposal

  1. Use the status set with c.status() in a loader, or in middleware before next(), for the page response. On a client visit the page should still render, since the status only matters to whoever made the request.
  2. Make a thrown HTTPException with no res render the nearest _error with its own status and message, the same way a 404 already renders _not-found. This should work from middleware too, through the root _error. Exceptions that carry their own res keep passing through unchanged, so a thrown redirect still works.

Either one covers the banned page. The second also covers maintenance, since the middleware can throw before any loader runs.

Prior art

  • TanStack Start: setResponseStatus(403) in a loader or server function.
  • React Router: return data(value, { status: 403 }) from a loader.
  • Next.js: forbidden() and unauthorized() render forbidden.tsx and unauthorized.tsx with 403 and 401, the same way notFound() renders not-found.tsx.

Activity

  1. self-assigned this
    on Nov 11, 2026
  2. kossnocorp commented on Nov 11, 2026

    @kossnocorp
    Contributor

    Thank you for reporting it, I'm looking into it.

  3. added 3 commits that reference this issue on Nov 13, 2026
    e9c4d68
    9462ce0
    e49f0f4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions