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
A plain error caught by <Errored> during SSR is serialized to the client verbatim (message, cause, own enumerable props; only .stack is stripped). The same error thrown by a server function over HTTP is sanitized to "Internal Server Error" outside the dev build (#3116). A "use server" function called in-process during SSR never touches dispatch, so the production page load leaks what the RPC wire withholds. Reported with a wire-level reproduction in #3415.
hydratedCreateErrorBoundary (packages/solid/src/client/hydration.ts) loads the record and rethrows it so the fallback hydrates against the same err(). The SSR codec (packages/web/serialization/src/serializer.ts) strips only Feature.ErrorPrototypeStack, keyed on NODE_ENV.
Other routes with the same leak: a createAsync rejection serialized into the stream (ctx.serialize(id, deferred.promise) rejects with the raw reason), and Errors nested in serialized data.
Constraint
The fallback is rendered on the server with the error. Whatever the client receives must be what the server rendered with, or the fallback mismatches on hydration. So the boundary must sanitize before renderFallback, not just before serialize.
Proposed fix
Boundary: outside the dev build, replace an unbranded error with a generic Error before rendering the fallback and serializing. markSafeError (Symbol.for("solid.SafeError"), a registered symbol — no @solidjs/web dependency needed) passes through. Dev/prod line is the build variant (server.dev.js behind development), matching sanitizeServerError. The original stays server-side: SSR_RENDER_ERROR_CONTAINED already carries it; add an SSR_ERROR_SANITIZED finding mirroring SERVER_FN_ERROR_SANITIZED.
Policy hook (follow-up, separate issue if preferred): a single server-side error hook, configured once, that every runtime-handled error passes through — contained SSR render error, rejected async source, server-function throw (direct or HTTP), uncontained render error — receiving { error, kind, boundary | functionId, event } and returning the wire value (undefined → default sanitization). This is the reporting seam as well as the mapping seam: today "report" exists only on the observe tier's diagnostics channel and "map" only for server functions via wrapInvocation. Needs once-per-error semantics (a direct server-function call that fails during SSR is both a server-function error and a contained render error; a <Loading> re-pull recurs the same throw). Two tiers, matching wrapInvocation: an ambient once-per-process registration (configure* in @solidjs/web — the only tier that sees direct in-process calls during SSR, per the existing entry-only caveat on the per-handler wrapInvocation), overridable per request. For vite-plugin-solid that means the ambient call goes in the serverFunctions.configure module and the per-request form rides the generated handler's existing handleRequest(request, { nonce, serverFunctions, ... }) options bag, threaded into the generated render(request, context) via context like clientEntry is today — no new plugin option needed. Note the plugin's generated DefaultErrorBoundary does console.error(props.error()) in the fallback, which today runs in the browser during hydration with the raw serialized error.
Tests
SSR document for an <Errored> catching new Error("secret") in the prod build carries no "secret"; fallback markup and serialized record agree; hydration is clean.
markSafeError passes through on the SSR path with own-properties.
Rejected createAsync serialized into the stream carries the sanitized reason.
Summary
A plain error caught by
<Errored>during SSR is serialized to the client verbatim (message,cause, own enumerable props; only.stackis stripped). The same error thrown by a server function over HTTP is sanitized to"Internal Server Error"outside the dev build (#3116). A"use server"function called in-process during SSR never touches dispatch, so the production page load leaks what the RPC wire withholds. Reported with a wire-level reproduction in #3415.Where
packages/solid/src/server/signals.ts,createErrorBoundary:hydratedCreateErrorBoundary(packages/solid/src/client/hydration.ts) loads the record and rethrows it so the fallback hydrates against the sameerr(). The SSR codec (packages/web/serialization/src/serializer.ts) strips onlyFeature.ErrorPrototypeStack, keyed onNODE_ENV.Other routes with the same leak: a
createAsyncrejection serialized into the stream (ctx.serialize(id, deferred.promise)rejects with the raw reason), and Errors nested in serialized data.Constraint
The fallback is rendered on the server with the error. Whatever the client receives must be what the server rendered with, or the fallback mismatches on hydration. So the boundary must sanitize before
renderFallback, not just beforeserialize.Proposed fix
Errorbefore rendering the fallback and serializing.markSafeError(Symbol.for("solid.SafeError"), a registered symbol — no@solidjs/webdependency needed) passes through. Dev/prod line is the build variant (server.dev.jsbehinddevelopment), matchingsanitizeServerError. The original stays server-side:SSR_RENDER_ERROR_CONTAINEDalready carries it; add anSSR_ERROR_SANITIZEDfinding mirroringSERVER_FN_ERROR_SANITIZED.Errorthe SSR serializer encodes (rejected promises, nested values), so the boundary is not the only guard — the shape of the result-graph walk in fix(web): sanitize failures that escape through the result graph #3113.{ error, kind, boundary | functionId, event }and returning the wire value (undefined → default sanitization). This is the reporting seam as well as the mapping seam: today "report" exists only on the observe tier's diagnostics channel and "map" only for server functions viawrapInvocation. Needs once-per-error semantics (a direct server-function call that fails during SSR is both a server-function error and a contained render error; a<Loading>re-pull recurs the same throw). Two tiers, matchingwrapInvocation: an ambient once-per-process registration (configure*in@solidjs/web— the only tier that sees direct in-process calls during SSR, per the existing entry-only caveat on the per-handlerwrapInvocation), overridable per request. Forvite-plugin-solidthat means the ambient call goes in theserverFunctions.configuremodule and the per-request form rides the generated handler's existinghandleRequest(request, { nonce, serverFunctions, ... })options bag, threaded into the generatedrender(request, context)viacontextlikeclientEntryis today — no new plugin option needed. Note the plugin's generatedDefaultErrorBoundarydoesconsole.error(props.error())in the fallback, which today runs in the browser during hydration with the raw serialized error.Tests
<Errored>catchingnew Error("secret")in the prod build carries no"secret"; fallback markup and serialized record agree; hydration is clean.markSafeErrorpasses through on the SSR path with own-properties.createAsyncserialized into the stream carries the sanitized reason.Refs: #3415, #3116, #3113, #3152, #3414.