Skip to content

Zero-argument server function calls 400 behind hosts that convert Node requests (empty POST body stream parses as an argument) #3214

Description

@ryansolid

Describe the bug

A zero-argument server function call fails with 400 Malformed server function arguments when the request reaches handleServerFunctionRequest through a host that converts a Node IncomingMessage into a web Request.

The client transport sends zero-argument calls as a bodyless POST (initializeResponse: args.length === 0createRequest with no body, no body-format tag), and the browser adds Content-Length: 0. The runtime detects "no arguments" via request.body === null:

// packages/web/server-functions/src/server.ts:1171 (parseArguments)
if (request.method === "POST" && request.body !== null) {
  const decoded = await extractBody(request.clone(), codec);
  ...
  if (decoded === undefined) {
    throw new TypeError("Server function body carries no usable encoding"); // :1234
  }

But Node-to-web conversions (Astro's dev/prod request handling, and the same pattern in every connect/Node adapter ecosystem) attach the socket stream as the body of every non-GET/HEAD request — including empty ones. So the empty POST arrives with request.body !== null, extractBody finds no format tag and no content type, answers undefined, and the guard from #3130 refuses the call: 400 on every zero-argument invocation.

Functions with arguments are unaffected (their bodies carry a format tag). The plugin's own dev middleware path doesn't hit it, which is why this only surfaces behind meta-framework hosts — found while building the Astro @astrojs/solid-js v8 adapter, where every zero-arg call through the injected endpoint route 400'd.

Reproduction

import { handleServerFunctionRequest, registerServerFunction } from '@solidjs/web/server-functions/server';

registerServerFunction('whoami-00000000', async () => 'me');

// What a Node-converted bodyless POST looks like: empty stream, Content-Length: 0.
const response = await handleServerFunctionRequest(
  new Request('http://localhost/_server/data/whoami-00000000', {
    method: 'POST',
    headers: { 'content-length': '0' },
    body: new ReadableStream({ start(c) { c.close(); } }),
    duplex: 'half',
  }),
);
// 400 — expected: 200 with the encoded 'me'

Same request with body: null answers 200.

Expected behavior

An empty POST body with no body-format tag is a zero-argument call, not a malformed one. parseArguments (or the dispatch ahead of it) could treat Content-Length: 0 as bodyless — or buffer the untagged/typeless body and skip the undefined-decode refusal when it is actually empty. The #3130 guard stays intact for tagged bodies and non-empty untagged ones.

Hosts can work around it by rebuilding the request when content-length is 0 (the Astro adapter now does), but every Node-based host conversion produces this shape, so the runtime accepting it would fix the whole class.

@solidjs/web 2.0.0-rc.5 / solid-js 2.0.0-rc.5.

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