Repository navigation
Description
Duplicates
- I have searched the existing issues
Latest version
- I have tested the latest version
Current behavior 😯
With Nitro, an error thrown by an API route or a middleware never reaches Nitro's error hook or its errorHandler. Error reporters that listen on that hook do not see these errors, and a custom errorHandler does not apply to them.
createBaseHandler (packages/start/src/server/handler.ts) serves the app through an h3 app of its own (new H3()). h3 turns whatever a handler throws into a response, so the handler's fetch never rejects. Nitro's SSR renderer gets h3's {"status":500,"unhandled":true,"message":"HTTPError"} back as an ordinary response, and Nitro's onError, which calls the error hook and then the errorHandler, does not run.
An error thrown by a Nitro route reaches the hook. In SolidStart 1.5.1 (vinxi 0.5.11 with nitropack 2.13.6), the same API route's error reached Nitro's error hook too.
Expected behavior 🤔
An unhandled error thrown by an API route or a middleware reaches Nitro's error hook and errorHandler, as an unhandled error of a Nitro route does.
Steps to reproduce 🕹
Failing test and proposed fix: #2379 (the first commit is the failing e2e test, the second the fix). The steps below reproduce the bug in a new app.
Steps:
- Run
pnpm create solid@latest -p repro -s --v2 -t basic --ts, thencd reproandpnpm add @solidjs/start@2.2.2 nitro@3.0.261103-rc. - Add
src/routes/api/throws.ts:export function GET() { throw new Error("thrown by an API route"); }
- Add
server-plugins/error-hook.ts:and pass it to Nitro inimport { definePlugin } from "nitro"; export default definePlugin(nitroApp => { nitroApp.hooks.hook("error", error => { console.log(`[error hook] ${error.message}`); }); });
vite.config.ts:nitro({ plugins: ["./server-plugins/error-hook.ts"] }). - Run
pnpm buildandnode .output/server/index.mjs, thencurl -i http://localhost:3000/api/throws. - The response is h3's JSON 500 and the server prints the error once (from h3), but
[error hook]is never printed. A Nitro route that throws (for exampleexport default () => { throw new Error("thrown by a Nitro route") }, added withhandlers: [{ route: "/nitro-throws", handler: "./server-routes/nitro-throws.ts" }]) prints[error hook] thrown by a Nitro route.
Context 🔦
We report server errors from Nitro's error hook, so errors from our API routes and middleware go unreported.
#1660 shows how an app can catch these errors itself, with an h3 middleware that wraps await next() in try/catch. That works when each app knows it needs that middleware and reports from it, or forwards the error with Nitro's captureError. Otherwise, integrations that capture server errors from Nitro's error hook do not see the error. Either way, Nitro's errorHandler never answers it.
Proposed fix. The fix, with unit tests, is in #2379. When a route, a middleware or the handler itself throws an error that h3 would answer as unhandled, the handler's fetch rejects with it, so the server hosting the handler answers and reports it. With Nitro, that means the error hook, then the errorHandler. These cases are answered as before:
- an h3
HTTPErrornot marked unhandled, or a status thrown as a number, by SolidStart; - an error reached through
request(), by SolidStart; - an error reached through an h3 app that the handler is mounted in, by that app.
SolidStart's own dev server (without Nitro) then shows its existing error overlay for such errors, and its preview server answers them with a 500.
Trade-offs.
- The handler's
fetchcan now reject, unlike h3'sH3.fetch. A server or adapter that calls it directly answers such an error itself. - The host builds the error response. With Nitro, the body is Nitro's (
{"error":true,"status":500,"unhandled":true}in production) and a customerrorHandlerapplies. An error that is not an h3HTTPErrorbut carries astatus, which h3 used for its answer, now gets the host's 500, as it would in a Nitro route. - Headers set for the error response with
event.res.errHeaderson SolidStart's event are not in the host's answer, because the host answers on its own event. Examples are headers from h3's CORS helpers and theCache-Control: no-storethathandleServerFunctionsets before it rethrows. So when SolidStart's middleware sets CORS headers with h3's helpers, a cross-origin client can no longer read such a 500. - An h3
HTTPErrornot marked unhandled (a 5xx too), or a status thrown as a number, is still answered by SolidStart, so it does not reach the hook, although in a Nitro route it would. - Server function requests that fail before the function runs, such as an unknown function id, are now reported as unhandled errors. They were already answered as unhandled 500s.
Alternatives. Two alternatives I considered keep SolidStart's answers as they are: calling Nitro's captureError when Nitro hosts the app (the hook runs, the errorHandler does not), or an error option on the handler, like serverFunctions.onError (#1660 declined one for createMiddleware).
Why the proposed fix. Nitro runs SolidStart as a service and handles a service's errors itself: its dev worker deliberately does not catch them where it calls a service ("like production services, they propagate to the caller", dev-worker.mjs). The proposed fix lets an unhandled error take that path, as 1.x did. Of the three approaches, it is the only one where both the hook and the errorHandler run, and it does not depend on Nitro: any server that calls the handler's fetch gets the error, SolidStart's own dev server included. The two alternatives keep SolidStart's 500 and its headers, but the first needs Nitro-specific code in SolidStart and leaves out the errorHandler, and the second changes nothing until each app or integration wires it up. The fix is in #2379, so that the approach can be judged with the code; if you prefer one of the alternatives or another approach, I will change the pull request.
Not covered. Errors thrown in a server function's body: SolidStart answers them by design and passes them to serverFunctions.onError. Errors thrown while rendering a page: SolidStart answers them with its own 500 page.
Your environment 🌎
System:
OS: Linux 6.18 Debian GNU/Linux 12 (bookworm) (Docker image node:24)
Binaries:
Node: 24.21.0
pnpm: 11.17.0
npmPackages:
@solidjs/start: 2.2.2
@solidjs/router: 1.0.0
nitro: 3.0.261103-rc
vite: 8.1.4
solid-js: 1.9.14
Activity