Repository navigation
Open
Description
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
- Use the status set with
c.status()in a loader, or in middleware beforenext(), for the page response. On a client visit the page should still render, since the status only matters to whoever made the request. - Make a thrown
HTTPExceptionwith noresrender the nearest_errorwith 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 ownreskeep 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()andunauthorized()renderforbidden.tsxandunauthorized.tsxwith 403 and 401, the same waynotFound()rendersnot-found.tsx.
Metadata
Metadata
Assignees
Labels
No labels
Type
Fields
Priority
None yet
Activity
kossnocorp commentedon Nov 11, 2026
Thank you for reporting it, I'm looking into it.
fix: allow `HTTPException` reach recovery code (closes #784)
fix: add test cases for #784
fix: allow `HTTPException` reach recovery code (closes #784)