You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 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.error and answers 500 with {"error":true,"status":500,"unhandled":true} as JSON. The headers and cookies the chain already wrote to the response stub are lost.
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 (as start.instrument in 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, and start.setup / start.renderMode failures.
A thrown control response turns into a 500. return redirect('/login') from a middleware works, but throw redirect('/login') reaches h3 as a non-Error rejection and becomes an unhandled 500. The server-function endpoint, served by the same handler, already treats a thrown Response as 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.middlewareexportdefaultasyncfunctionmiddleware(request: Request,next: ()=>Promise<Response>){if(newURL(request.url).pathname==='/boom')thrownewError('token=abc123')returnnext()}
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 (except Response.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 configureServerErrors symbol. A public entry point in @solidjs/web for request failures, with its own kind in ServerErrorSite, 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.
Problem
In Start mode, the generated
handleRequestruns thestart.middlewarechain without a catch:solid-vite-plugin/src/ssr/index.ts
Lines 1433 to 1453 in 52d93eb
When a middleware throws or rejects in a production build, the exception leaves
handleRequestand each host deals with it in its own way: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).examples/start-ssrserver sendse.messageback to the client (examples/start-ssr/server.js#L78-L81).Two consequences go beyond the inconsistent response:
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.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-plugin3.0.0-next.46,@solidjs/web2.0.0-rc.11 and Nitro 3 beta:Build for production, serve it and request
/boom: the response is Nitro's JSON 500, the server log showsError: token=abc123with its stack, and a hook registered withconfigureServerErrorsis not called.Expected
In production builds, the generated handler contains request failures the way the server-function endpoint does:
Response(exceptResponse.error()) or response envelope becomes the response;Notes
configureServerErrorssymbol. A public entry point in@solidjs/webfor request failures, with its ownkindinServerErrorSite, would remove that coupling.I'll follow up with a PR for the containment part.