[2.0 rc.21 / next] renderToStream: a value that fails to serialize inside the write before the first flush still sends the shell and calls onCompleteAll
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 54/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- javascript, typescript
Línea de trabajo
Start with packages/web/src/server.ts around the renderToStream serializer onError condition identified in the issue, then read the existing stream serialization tests and the serialize entry in documentation/solid-2.0/12-ssr-http.md. Reproduce the synchronous hand-over failure before the first flush and compare it with the async case. Done means the pre-flush behavior matches the expected failed render and the affected tests and documentation reflect the chosen behavior.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Follow-up to #4647 and #4659. Since #4659, when the default serializer fails on an async value before the first flush, the render fails: nothing is sent, onCompleteAll does not fire and createSSRResponse answers 500. When the value fails while it is being handed to the serializer instead (a deferStream value, or getHydrationWriter().write with a settled value), also before the first flush, the hook hears the same render/serialize failure, but the render completes: the shell goes out without that key's hydration record, a piped stream ends normally, onCompleteAll runs and createSSRResponse answers 200.
For comparison, a deferStream value with no <Errored> around it that rejects before the first flush already fails the render (handling: "failed", nothing sent). Resolving to a value that cannot be serialized is what lets it complete.
Versions
solid-js, @solidjs/web and @solidjs/signals 2.0.0-rc.21 from npm (@solidjs/web pins seroval and seroval-plugins to 1.7.0), Node 26.13.0. I did not build next, but the condition quoted below is unchanged in its source at cf839ea.
Reproduction
Save as repro.mjs in a project with solid-js and @solidjs/web 2.0.0-rc.21, then run it with sync or async, and await or pipe:
import { renderToStream, ssr, escape } from "@solidjs/web";
import { createMemo } from "solid-js";
// "sync": deferStream holds the shell, then the resolved value is written and
// fails inside the write. "async": the pending promise is written and fails
// when it settles. Both failures happen before anything is sent.
const mode = process.argv[2] ?? "sync";
const consume = process.argv[3] ?? "await";
class Unserializable {}
function App() {
const ok = createMemo(() => Promise.resolve("ok"), { deferStream: true });
const value = createMemo(() => Promise.resolve(new Unserializable()), {
deferStream: mode === "sync"
});
return ssr(["<main>", "</main>"], () => escape(`${ok()} ${value() instanceof Unserializable}`));
}
let completedAll = false;
const errors = [];
const stream = renderToStream(App, {
onCompleteAll: () => (completedAll = true),
onError: (error, info) => errors.push(`${error?.constructor?.name} (${info?.handling})`)
});
const summary = out => ({
main: /<main>.*?<\/main>/.exec(out)?.[0] ?? null,
keys: [...out.matchAll(/_\$HY\.r\["([^"]+)"\]/g)].map(m => m[1])
});
if (consume === "pipe") {
let sent = "";
let ended = false;
stream.pipe({ write: chunk => (sent += chunk), end: () => (ended = true) });
await new Promise(resolve => setTimeout(resolve, 200));
console.log(mode, consume, JSON.stringify({ ...summary(sent), ended, completedAll, errors }));
} else {
const html = await stream;
console.log(mode, consume, JSON.stringify({ ...summary(html), completedAll, errors }));
}
Observed
$ node repro.mjs sync await
sync await {"main":"<main>ok true</main>","keys":["0"],"completedAll":true,"errors":["SerovalUnsupportedTypeError (serialize)"]}
$ node repro.mjs sync pipe
sync pipe {"main":"<main>ok true</main>","keys":["0"],"ended":true,"completedAll":true,"errors":["SerovalUnsupportedTypeError (serialize)"]}
$ node repro.mjs async await
async await {"main":null,"keys":[],"completedAll":false,"errors":["SerovalUnsupportedTypeError (serialize)"]}
$ node repro.mjs async pipe
async pipe {"main":null,"keys":[],"ended":true,"completedAll":false,"errors":["SerovalUnsupportedTypeError (serialize)"]}
In sync, key 0 (the ok memo) is in the document and key 1 (the failing memo) is not; with a serializable value in its place, both keys are written. node --conditions=development prints the same lines.
With the same App, this shows the status a server would send:
import { createRequestEvent, createSSRResponse } from "@solidjs/web";
import { provideRequestEvent } from "@solidjs/web/storage";
const event = createRequestEvent(new Request("https://example.com/"));
const response = await provideRequestEvent(event, () =>
createSSRResponse(renderToStream(App, { onError() {} }), event)
);
const body = await response.text();
console.log(mode, response.status, JSON.stringify(/<main>.*?<\/main>/.exec(body)?.[0] ?? body));
sync 200 "<main>ok true</main>"
async 500 ""
Expected
Before the first flush, sync behaves like async: the render fails, nothing is sent, onCompleteAll is not called and createSSRResponse answers 500. (With pipe, onCompleteShell still runs before the render is abandoned, as it already does in the async case.) This report does not ask for any change after the first flush.
Cause
renderToStream marks the render as stranded only when the failure arrives outside a write (packages/web/src/server.ts L2572-L2575 on next):
onError: err => {
reportFailure(err, "serialize");
if (!writing && !options.serializer && !firstFlushed) stranded = true;
},
seroval reports a value it cannot parse synchronously, from inside serializer.write. handOver counts that call in writing, so for a deferStream value (written once its promise resolves) or a getHydrationWriter().write the condition is false and the render goes on. This is deliberate: the comment above writing (L2365-L2373) says "Keep synchronous failures completing as before (#4227)", #4659 lists it among the unchanged cases ("A failure during hand-over still completes (#4227), because that value never reaches the client"), and the serialize entry in 12-ssr-http.md says "A value that fails as it is handed over never reaches the client, and the render goes on without it". The published rc.21 has the same condition in dist/server.js, dist/server.dev.js and dist/server.observe.js.
Why the carve-out should not cover the window before the first flush
- It is older than the first-flush split. Completing is what #4229 made every serialization failure do, back when that was the only outcome. My draft #4650 left synchronous failures as they were, and #4659 kept that when it moved the line to the first flush, following the ruling quoted in its description ("failing ssr on failed serialization feels reasonable otherwise we could be midstream"). The comment's own premise, "Before the first flush nothing has reached the client, so the render fails", holds for a failure inside
writetoo: untilpipe/pipeTosetsfirstFlushed, the shell and the hydration tasks are still buffered, and an awaited render never sets it. - "That value never reaches the client" does not leave a whole document. The shell was rendered from the value (
ok true) but has no record for its key. When hydrating, the memo's read takes thecomputebranch (readSerializedOrComputeinsolid-jscallscomputewhen!sharedConfig.has(id)), so the computation runs again on the client and its result can differ from what the server rendered. For the async case, #4659 settled on "Nothing is refetched on hydration", and adeferStreamvalue exists to be in the document before the shell. - The same API gets two outcomes.
stream-batch-serialize-failure-4242.spec.tsxpins that agetHydrationWriter().writeof a promise that resolves to an unserializable value, before the shell, fails the render (no HTML, and the siblinglib:fineis lost).stream-sync-serialize-failure-4227.spec.tsxpins that the same write with a settled value completes and keeps its siblings. - The hook cannot tell them apart. Both failures arrive as
{ kind: "render", handling: "serialize" }; one ends in a failed render, the other in a 200 shell andonCompleteAll. To keep that shell from going out, a server would have to wrap the sink and check a flag set by the hook before the first write. - The comment also notes that a buffered stream can emit its initial chunk and fail inside
write. Before the first flush that chunk is still in the task buffer; after it, the condition is already off.
Possible change
Drop !writing from that condition:
if (!options.serializer && !firstFlushed) stranded = true;
We carry exactly this one-line change on rc.21 as a local patch (dist/server.js and dist/server.dev.js). With it, the same scripts print:
sync await {"main":null,"keys":[],"completedAll":false,"errors":["SerovalUnsupportedTypeError (serialize)"]}
sync pipe {"main":null,"keys":[],"ended":true,"completedAll":false,"errors":["SerovalUnsupportedTypeError (serialize)"]}
sync 500 ""
and the async lines do not change.
What it covers: like the async case today, the failure fails the whole render before the first flush, even when the value is read inside an <Errored> (an async value that fails there already does this). It applies to every synchronous hand-over, so a library that writes a settled value it cannot serialize through getHydrationWriter().write would fail the page instead of dropping one key. It changes stream-sync-serialize-failure-4227.spec.tsx, which pins completion for a deferStream value and for getHydrationWriter().write, and the serialize entry in 12-ssr-http.md. The other specs on next that check render/serialize (stream-ambient-error-hook-4193, stream-batch-serialize-failure-4242, stream-async-serialize-failure-4647 and concurrent-render-error-policy) use async values, which already fail outside write. If completing on a settled library write is wanted, the carve-out could stay for getHydrationWriter() only, though an async write through the same API already fails the render before the first flush.
After the first flush, a value that fails inside the write still never reaches the client, so the client computes that key again, while an async value that fails there is rejected on the client (#4659). That gap is outside this report.
Related
- #4227 and #4229: a synchronous failure used to hang the render; fixed by completing it.
- #4647, #4650 and #4659: an async failure fails the render before the first flush and rejects the key on the client after it.
- #4193 and #4206: serializer failures reach the error hook.
- #4242 and #4246: one failing value no longer strands the pre-shell batch.
- Lenguaje dominante
- TypeScript
- Estrellas
- 36.2k
- Forks
- 1.1k
- Merge medio
- 9 h
- PR fusionados (30 d)
- 463
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de solidjs/solid
-
`<style>` and `<script>` children inside MathML are not escaped in SSR (and static DOM templates), allowing markup injectionPosiblemente ocupada @santhiprakash la tomó hoy. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
solidjs/solid#4689 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
solidjs/solid#4685 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
1.x
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
Los mantenedores suelen responder en 1 día
Todos los issues de solidjs/solid
Issues similares
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Yeraze/meshmonitor#6239 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
better-auth/better-auth#12292 · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
The sample email preview's ResizeObserver is never disconnected and keeps raising errors after leaving SettingsPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
area:ui bug effort:S priority:P2
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Typo in WSL project description on Projects pagePosiblemente ocupada @akhil9tiet la tomó hoy. Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 75/100
microsoft/opensource.microsoft.com#1066 · 1 comentario ·