Describe the bug
When the codec cannot encode a result, the caller receives undefined — the same answer a void function gives. The function already ran and committed its side effects; only the encoding failed.
It fails after the head is committed, so the status is spent (200) and no error tag can be added. The body stops mid-stream, and a truncated Serialized body that carried no complete frame decodes to undefined.
So a write that succeeded is indistinguishable from one that returned nothing. A UI renders an empty state; a data layer may retry a non-idempotent mutation.
caller got: undefined | a genuinely void function also gives: undefined | fn ran: 1
Reaching it does not take anything exotic — a throwing getter, a value the codec cannot represent, a plugin that fails.
Steps to reproduce
# Run against a build of the `next` BRANCH. The published `next` dist-tag is
# 2.0.0-rc.4, which predates the `<endpoint>/data/<id>` address (#3094) and
# answers 404 to every request below.
node repro.mjs
repro.mjs:
import { AsyncLocalStorage } from "node:async_hooks";
globalThis[Symbol.for("solid.RequestContext")] = new AsyncLocalStorage();
const srv = await import("@solidjs/web/server-functions/server");
const cli = await import("@solidjs/web/server-functions/client");
let ran = 0;
srv.registerServerFunction("bad-encode", async () => {
ran++; // the function SUCCEEDED and committed
return { ok: true, get unencodable() { throw new Error("cannot encode"); } };
});
srv.registerServerFunction("void-fn", async () => undefined);
// a socket, not an in-process call: a stream that errors arrives TRUNCATED
globalThis.fetch = async (address, init) => {
const request = new Request(new URL(address.toString(), "http://localhost"), init);
request.headers.set("Sec-Fetch-Site", "same-origin");
const response = await srv.handleServerFunctionRequest(request);
const chunks = [];
try { for await (const chunk of response.body ?? []) chunks.push(chunk); } catch {}
return new Response(chunks.length ? Buffer.concat(chunks) : null,
{ status: response.status, headers: response.headers });
};
console.log("caller got:", await cli.createServerReference("bad-encode")());
console.log("a void function gives:", await cli.createServerReference("void-fn")());
console.log("fn ran:", ran);
The buffering matters: in-process the error surfaces live, which is why this is invisible in a test harness and not in a deployment.
Expected behavior
A result that could not be delivered reaches the caller as a failure, not as an empty success.
Options
- Make a zero-frame
Serialized body an error on decode. deserializeStream (shared.ts:1071-1106) answers undefined when the first chunk is already done; a Serialized format that carried no frame is a truncation, not a void result — the void case has its own format. One check there, and it converts silent loss into a rejection.
- Say so on the wire while the head is still open. Nothing can be added once the body has started, but the encoder knows what it is about to attempt; a failure that is knowable before the first frame could still take the ordinary 500 path.
- Both — (1) covers the truncation whatever caused it, including a wire cut, and (2) improves the message when the runtime knew early.
- Document it. Cheapest and worst: the failure mode is a silently emptied result.
I lean to (1) as the fix and (2) as the improvement.
Related
A failing test for this is in #3112. #3116 is the other half of the same "the head is already committed" problem — there the failure is delivered, unsanitized. A fix for this one should check it does not simply hand that leak a new road.
Describe the bug
When the codec cannot encode a result, the caller receives
undefined— the same answer a void function gives. The function already ran and committed its side effects; only the encoding failed.It fails after the head is committed, so the status is spent (200) and no error tag can be added. The body stops mid-stream, and a truncated
Serializedbody that carried no complete frame decodes toundefined.So a write that succeeded is indistinguishable from one that returned nothing. A UI renders an empty state; a data layer may retry a non-idempotent mutation.
Reaching it does not take anything exotic — a throwing getter, a value the codec cannot represent, a plugin that fails.
Steps to reproduce
repro.mjs:The buffering matters: in-process the error surfaces live, which is why this is invisible in a test harness and not in a deployment.
Expected behavior
A result that could not be delivered reaches the caller as a failure, not as an empty success.
Options
Serializedbody an error on decode.deserializeStream(shared.ts:1071-1106) answersundefinedwhen the first chunk is alreadydone; aSerializedformat that carried no frame is a truncation, not a void result — the void case has its own format. One check there, and it converts silent loss into a rejection.I lean to (1) as the fix and (2) as the improvement.
Related
A failing test for this is in #3112. #3116 is the other half of the same "the head is already committed" problem — there the failure is delivered, unsanitized. A fix for this one should check it does not simply hand that leak a new road.