Skip to content

A result the codec cannot encode reaches the caller as undefined #3117

Description

@frenzzy

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

  1. 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.
  2. 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.
  3. Both — (1) covers the truncation whatever caused it, including a wire cut, and (2) improves the message when the runtime knew early.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions