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
@ryansolid, I'm adding an error page in Solid 2 and would like to keep it as simple as the data-loading examples. Can one Errored boundary provide useful feedback and a way to try again?
I built solid-error-repro to test backend failures using a local production build. During SSR, the document sent to the browser contains the backend's complete synthetic HTML error page serialized as an escaped Error.message. The fallback never renders it. Calling the same server function from the browser returns a short generic error instead.
My concern is that sensitive backend diagnostics could reach the browser even when the user sees only a generic error page.
What I tested
A separate local backend returns either a structured error or a synthetic HTML error page of 65,758 bytes. The app calls it through one use server function, with one global Errored boundary. Unique markers are generated after the frontend build, and the tests check that none appear in the client assets.
For the HTML case, I deliberately put the response body into the error:
fetch() does not throw on HTTP 500 by itself; this application code puts the body into the error.
With solid-js and @solidjs/web 2.0.0-rc.8, plugin next.43, Router next.24 and Vite 8.3.0:
Request
Response
Initial SSR with the HTML error
HTTP 500 document, 67,403 decoded bytes. Its hydration script contains the complete upstream HTML as an escaped Error.message string.
Streaming SSR with the same error
HTTP 200 document, 68,861 decoded bytes. The same error body arrives after the loading shell.
Browser calls the same server function
HTTP 200 document, 842 decoded bytes. The subsequent function request returns HTTP 500 with an 80-byte serialized error body containing Internal Server Error and no backend markers.
Initial SSR after the loader explicitly changes the message to Request failed
HTTP 500 document, 1,618 decoded bytes, without the upstream markers.
The backend HTML is inside the serialized Error.message in the SSR document. Its < characters are escaped as \x3C; the fallback does not render that HTML. Each marker appears exactly once, and a SHA-256 check confirms that the decoded message matches the complete backend response body.
The table shows decoded body sizes. An offline gzip check reduces the 67.4 KB document to about 1.46 KB and the short-message document to about 0.98 KB, since the synthetic text is repetitive. I have not measured transfer latency. My main question is why the full error reaches the browser through SSR when the server-function HTTP response omits it.
The structured error behaves the same way: the SSR payload includes its message, cause, and a custom property named internalContext. The server-function HTTP response omits them. All values are synthetic. This follows up on the SSR question in #3116.
Risk of exposing backend data
If an upstream error contains personal data, credentials, or details of internal requests, serializing it could disclose those values to whoever receives the SSR response. Internal details could include service hostnames, endpoint URLs, request headers or bodies. This depends on those values being present in the error's message, cause, or serialized properties; the reproduction does not show arbitrary backend calls being exposed.
Escaping the HTML keeps it as a string. Its content is still readable in the document response using developer tools or an HTTP client. Showing only "Something went wrong" in the fallback does not keep error details private once they have been sent to the browser.
The reproduction uses synthetic markers to demonstrate that path from a backend error to the browser. No real credentials or personal data were used or exposed in these tests.
Questions
Should production SSR send the full error message, cause, and custom properties to the browser by default, given that they can contain sensitive backend data and the fallback may never display them?
Is there a central place to choose which error details reach the browser? I'd like to keep diagnostics in server logs while still allowing useful messages, such as validation errors, to reach the user. Doing this once would avoid repeating the same checks in every loader.
With query(), what is the simplest supported way to retry from an error page? In the client case, the fallback counter works, but calling reset() made no new backend request during a 750 ms observation window after the backend recovered. Is the intended approach to invalidate the query as well as reset the boundary?
A normal link that reloads the page works in this example. The tests confirm that reloading the SSR page sends another request while the backend is still failing, and that the page recovers once the backend responds normally. This also works with JavaScript disabled. For a request started by a button, the user has to click that button again after reloading. An example showing how to add retry to the usual data-loading pattern would help.
The repository also reproduces a separate problem: a counter inside the initial SSR fallback does not update when clicked. pnpm repro:bug demonstrates it without Router, including with a synchronous throw. I reported that behavior in #3414. I have not established a connection between it and error serialization or query retry.
The suite covers eight response/reload cases, one check of the client assets, and two reset cases. Each ran three times, for 33 passing runs. These tests assert the observed behavior; pnpm repro:bug separately fails on the two unresponsive counter cases. Raw bodies, browser errors, screenshots, and traces from failed tests are saved under test-results/<run>/.
Thanks — the reproduction is precise and the conclusion is right. Answering the three questions and saying where this goes.
1. Should production SSR ship message / cause / own-properties by default? No. There are two wires for the same failure today and they disagree:
Server-function HTTP dispatch sanitizes a plain thrown value outside the dev build ("Internal Server Error", markSafeError to opt out) — #3116.
SSR does not. The server <Errored> boundary serializes the error it caught, verbatim, so the client boundary can rethrow it during hydration and the fallback hydrates against the same err(). Only .stack is stripped; message, cause, and own enumerable props travel. A "use server" fu…
SSR does not. The server <Errored> boundary serializes the error it caught, verbatim, so the client boundary can rethrow it during hydration and the fallback hydrates against the same err(). Only .stack is stripped; message, cause, and own enumerable props travel. A "use server" function called in-process during SSR never touches dispatch, so it lands on the boundary raw — exactly the asymmetry your table shows. The same is true of a createAsync rejection serialized into the stream (your streaming 200 case).
That is a gap, not a design choice we are keeping. The fix has a constraint: the fallback is rendered on the server with the error, so whatever the client receives has to be what the server rendered with, or the fallback mismatches on hydration. So sanitization has to happen before the fallback renders, not just before serialization, and the SSR serializer also needs to apply the same policy to errors reaching it by other routes (rejected promises, errors nested in data). The dev/prod line will be the build variant (solid-js and @solidjs/web both ship server.dev.js behind the development condition), same as server functions — not NODE_ENV. markSafeError will be honored on the SSR path too. Tracking in #3468.
2. A central place to decide what reaches the browser. Doesn't exist yet, and the fix above is only the safe default. The intended shape is one server-side error hook that every error the runtime handles passes through — contained by <Errored> during SSR, a rejected async source, a server-function throw (direct or over HTTP), an uncontained render error — receiving the original plus context (kind, boundary id or function id, request event) and returning what the wire may carry (undefined = default sanitization). That is the SvelteKit handleError shape: report to your telemetry and shape the client-facing error in the same function, instead of repeating it in every loader. It has to be one hook because those two jobs happen at the same moment; today the "report" half is only on the observe build's diagnostics channel, and the "map" half only exists for server functions (wrapInvocation). It would live in @solidjs/web with the same two tiers the server-function config already has: registered once for the process (the configureServerFunctionsServer pattern — the only tier that can see direct in-process calls made during SSR), overridable per request through the Vite plugin's handleRequest(request, { ... }) options bag, next to nonce and serverFunctions.
Until then: markSafeError(new Error("Request failed")) at the throw site is the supported opt-in for messages meant for the user.
3. Retry with query(). This is the router, by design: query caches the rejected promise for the 5 s preload window (#385 — an error from a cached preload stays an error), so reset() within that window re-creates the children, hits the cache, and gets the same rejection with no request. The supported pattern is to invalidate first:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
@ryansolid, I'm adding an error page in Solid 2 and would like to keep it as simple as the data-loading examples. Can one
Erroredboundary provide useful feedback and a way to try again?I built solid-error-repro to test backend failures using a local production build. During SSR, the document sent to the browser contains the backend's complete synthetic HTML error page serialized as an escaped
Error.message. The fallback never renders it. Calling the same server function from the browser returns a short generic error instead.My concern is that sensitive backend diagnostics could reach the browser even when the user sees only a generic error page.
What I tested
A separate local backend returns either a structured error or a synthetic HTML error page of 65,758 bytes. The app calls it through one
use serverfunction, with one globalErroredboundary. Unique markers are generated after the frontend build, and the tests check that none appear in the client assets.For the HTML case, I deliberately put the response body into the error:
fetch()does not throw on HTTP 500 by itself; this application code puts the body into the error.With
solid-jsand@solidjs/web2.0.0-rc.8, plugin next.43, Router next.24 and Vite 8.3.0:Error.messagestring.Internal Server Errorand no backend markers.Request failedThe backend HTML is inside the serialized
Error.messagein the SSR document. Its<characters are escaped as\x3C; the fallback does not render that HTML. Each marker appears exactly once, and a SHA-256 check confirms that the decoded message matches the complete backend response body.The table shows decoded body sizes. An offline gzip check reduces the 67.4 KB document to about 1.46 KB and the short-message document to about 0.98 KB, since the synthetic text is repetitive. I have not measured transfer latency. My main question is why the full error reaches the browser through SSR when the server-function HTTP response omits it.
The structured error behaves the same way: the SSR payload includes its message,
cause, and a custom property namedinternalContext. The server-function HTTP response omits them. All values are synthetic. This follows up on the SSR question in #3116.Risk of exposing backend data
If an upstream error contains personal data, credentials, or details of internal requests, serializing it could disclose those values to whoever receives the SSR response. Internal details could include service hostnames, endpoint URLs, request headers or bodies. This depends on those values being present in the error's message, cause, or serialized properties; the reproduction does not show arbitrary backend calls being exposed.
Escaping the HTML keeps it as a string. Its content is still readable in the document response using developer tools or an HTTP client. Showing only "Something went wrong" in the fallback does not keep error details private once they have been sent to the browser.
The reproduction uses synthetic markers to demonstrate that path from a backend error to the browser. No real credentials or personal data were used or exposed in these tests.
Questions
query(), what is the simplest supported way to retry from an error page? In the client case, the fallback counter works, but callingreset()made no new backend request during a 750 ms observation window after the backend recovered. Is the intended approach to invalidate the query as well as reset the boundary?A normal link that reloads the page works in this example. The tests confirm that reloading the SSR page sends another request while the backend is still failing, and that the page recovers once the backend responds normally. This also works with JavaScript disabled. For a request started by a button, the user has to click that button again after reloading. An example showing how to add retry to the usual data-loading pattern would help.
The repository also reproduces a separate problem: a counter inside the initial SSR fallback does not update when clicked.
pnpm repro:bugdemonstrates it without Router, including with a synchronous throw. I reported that behavior in #3414. I have not established a connection between it and error serialization or query retry.Reproduce and inspect
pnpm install --frozen-lockfile pnpm exec playwright install chromium REPRO_RUN=matrix pnpm verify pnpm report matrixThe suite covers eight response/reload cases, one check of the client assets, and two reset cases. Each ran three times, for 33 passing runs. These tests assert the observed behavior;
pnpm repro:bugseparately fails on the two unresponsive counter cases. Raw bodies, browser errors, screenshots, and traces from failed tests are saved undertest-results/<run>/.Related: #3116 on serialization, #3152 on stack traces, and discussion #2658 on reset and retry.
The captured SSR document and server-function error body are included with their metadata.
All reactions