Repository navigation
Middleware exceptions escape the generated handleRequest and bypass configureServerErrors #382
Description
Problem
In Start mode, the generated handleRequest runs the start.middleware chain without a catch:
solid-vite-plugin/src/ssr/index.ts
Lines 1433 to 1453 in 52d93eb
| `export async function handleRequest(request, options = {}) {`, | |
| // `options.event` is the public wrapper->event extension seam: extra | |
| // fields (conventionally `nativeEvent`, the platform's raw request | |
| // object) spread over the event's defaults at creation, so hosts and | |
| // custom server entries can extend what getRequestEvent() answers | |
| // with — no new convention beyond createRequestEvent's own init | |
| // parameter (spreading undefined is a no-op). | |
| ` const event = createRequestEvent(request, options.event);`, | |
| // Middleware runs inside the request scope, after event creation — | |
| // getRequestEvent() answers in middleware exactly as in app code, and | |
| // nothing reaches the wire until the outermost middleware returns. | |
| ` const response = await provideRequestEvent(event, () =>`, | |
| ` runMiddleware(request, (req) => dispatchRequest(req || request, event, options)),`, | |
| ` );`, | |
| // The fold runs strictly AFTER the outermost middleware returned: | |
| // headers stay mutable through the whole unwind, and a middleware | |
| // early return (an API handler that never called next()) gets its | |
| // stub writes — cookies set inside the request scope, status — onto | |
| // the wire. Unconditional: page responses come back from | |
| // createSSRResponse committed and pass through untouched. | |
| ` return commitEventResponse(response, event);`, |
When a middleware throws or rejects in a production build, the exception leaves handleRequest and each host deals with it in its own way:
- Nitro (h3) logs the original error with
console.errorand answers500with{"error":true,"status":500,"unhandled":true}as JSON. The headers and cookies the chain already wrote to the response stub are lost. start.nodelogs it and answers a plain-text 500 (src/node-entry/index.ts#L154-L161).- The
examples/start-ssrserver sendse.messageback to the client (examples/start-ssr/server.js#L78-L81).
Two consequences go beyond the inconsistent response:
- The hook registered with
configureServerErrors({ onError })never sees these failures, although its docs describe it as seeing "the failure that fails a request". Apps that rely on it to keep original errors out of logs, or to forward them to an APM (asstart.instrumentin feat: start.instrument — a server module awaited to completion before the handler graph loads #365 suggests), lose that for middleware, API handlers mounted in middleware, andstart.setup/start.renderModefailures. - A thrown control response turns into a 500.
return redirect('/login')from a middleware works, butthrow redirect('/login')reaches h3 as a non-Errorrejection and becomes an unhandled 500. The server-function endpoint, served by the same handler, already treats a thrownResponseas the response.
Reproduction
With @solidjs/vite-plugin 3.0.0-next.46, @solidjs/web 2.0.0-rc.11 and Nitro 3 beta:
// the module passed to start.middleware
export default async function middleware(request: Request, next: () => Promise<Response>) {
if (new URL(request.url).pathname === '/boom') throw new Error('token=abc123')
return next()
}Build for production, serve it and request /boom: the response is Nitro's JSON 500, the server log shows Error: token=abc123 with its stack, and a hook registered with configureServerErrors is not called.
Expected
In production builds, the generated handler contains request failures the way the server-function endpoint does:
- a thrown
Response(exceptResponse.error()) or response envelope becomes the response; - anything else goes through the configured server error policy and yields a generic 500 that still carries the stub's headers and cookies;
- dev keeps the current behavior, so the Vite overlay still shows the original error.
Notes
- There is no public API today to run a value through the configured policy outside a render, so the handler would have to call the hook registered under the
configureServerErrorssymbol. A public entry point in@solidjs/webfor request failures, with its ownkindinServerErrorSite, would remove that coupling. - A later step could render the app's error boundary when the chain fails before the page render, so a failed navigation gets an HTML error page instead of an empty 500. Apps can do that in userland today, but it depends on the single-render rule and on excluding the server-function endpoint.
- Related: Define the portable application deployment adapter contract #328, since a deploy adapter would benefit from a handler that does not reject for application failures.
I'll follow up with a PR for the containment part.
Activity
fix: contain request failures at the Start handler edge
ryansolid commentedon Oct 8, 2026
Fixed by #383 (merged to
next): failures escaping thestart.middlewarechain are now settled at the handler edge. A thrownResponse/respond()envelope becomes the response, and in production builds other failures go throughconfigureServerErrors(orconsole.error) and answer a bodyless 500. Will ship in the nextnextrelease.