Repository navigation
Expose per-render failure state for buffered SSR consumers #4125
Description
Use case
A buffered SSR consumer needs to know whether its HTML contains a render failure before publishing it as a successful document. Our concrete use case is serving Markdown from the same rendered page HTML, without browser hydration.
Awaiting renderToStream() intentionally resolves with the HTML produced, including a render where a Loading fragment rejected. onCompleteAll also runs for that case. The documented onError hook reports/maps each error object once, so it cannot act as a per-render integrity signal:
- Reusing one Error in two requests only notifies the first request.
- A server-function error first reported as
kind: 'server-function'is not reported again askind: 'render'when it escapes into Loading.
The distinction matters because a server-function failure caught by application code may still produce valid content. Marking every server-function failure would reject those valid renders.
Reproduction
Verified against solid-js and @solidjs/web 2.0.0-rc.15. Run this as an ES module in Node with those packages installed:
import {configureServerErrors,createRequestEvent,renderToStream,ssr,markSafeError} from '@solidjs/web';
import {provideRequestEvent} from '@solidjs/web/storage';
import {configureServerFunctionsServer,registerServerReference,createServerReference} from '@solidjs/web/server-functions/server';
import {createComponent,createMemo,Loading} from 'solid-js';
const calls=[];
configureServerErrors({onError(error,context){calls.push({kind:context.kind,handling:context.handling,url:context.event?.request.url}); if(context.kind==='render'&&context.event) context.event.locals.hasSsrRenderFailure=true; return markSafeError(new Error('Public failure'));}});
configureServerFunctionsServer({wrapInvocation:async run=>{try{return await run()}catch{return (()=>{throw markSafeError(new Error('Public failure'));})()}}});
async function render(id, run){
const event=createRequestEvent(new Request(`https://example.test/${id}`));
const before=calls.length; let didCompleteAll=false;
const html=await provideRequestEvent(event,()=>renderToStream(()=>ssr(['<main>', '</main>'],createComponent(Loading,{fallback:'pending',get children(){const data=createMemo(run);return ssr(['<p>','</p>'],createMemo(()=>data()))}})), {onCompleteAll(){didCompleteAll=true}}));
return {id,didCompleteAll,hasFailure:event.locals.hasSsrRenderFailure??false,hasResolved:html.includes('resolved'),calls:calls.slice(before)};
}
const shared=new Error('PRIVATE_shared');
const results=[];
results.push(await render('reuse-first',async()=>{throw shared}));
results.push(await render('reuse-second',async()=>{throw shared}));
const sf=createServerReference(registerServerReference('probe-failure',async()=>{throw new Error('PRIVATE_sf')}));
results.push(await render('server-function',()=>sf()));
console.log(JSON.stringify(results,null,2));Observed output:
[
{
"id": "reuse-first",
"didCompleteAll": true,
"hasFailure": true,
"hasResolved": false,
"calls": [
{
"kind": "render",
"handling": "client",
"url": "https://example.test/reuse-first"
}
]
},
{
"id": "reuse-second",
"didCompleteAll": true,
"hasFailure": false,
"hasResolved": false,
"calls": []
},
{
"id": "server-function",
"didCompleteAll": true,
"hasFailure": false,
"hasResolved": false,
"calls": [
{
"kind": "server-function",
"handling": "thrown",
"url": "https://example.test/server-function"
}
]
}
]All three renders complete with the resolved fragment missing. The second and third renders have no render-hook notification. The existing hook behavior matches its once-per-error-object contract; invoking a mapper again is not the requested solution.
Proposed direction
Expose a read-only, monotonic failure flag on each renderToStream() result. Set it at render containment/failure sites independently of error-reporting deduplication, without retaining or exposing error payloads. Consumers can await the render, check this flag and the request lifecycle, and reject incomplete documents.
A contribution is being prepared against next, with coverage for repeated errors across requests, server-function failures that escape versus those caught by application code, contained fallbacks, serialization, concurrent renders and normal streaming. Existing sanitization and hook invocation counts should remain unchanged.
Activity
fix(server): keep an error's thrower per request/render scope, not pe…
fix(server): the error hook hears an error once per kind, so a render…
fix(server): the server error hook hears an error once per kind per r…
ryansolid commentedon Oct 18, 2026
Generated by Claude Code