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 === 0 → createRequest 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.
Describe the bug
A zero-argument server function call fails with
400 Malformed server function argumentswhen the request reacheshandleServerFunctionRequestthrough a host that converts a NodeIncomingMessageinto a webRequest.The client transport sends zero-argument calls as a bodyless POST (
initializeResponse:args.length === 0→createRequestwith no body, no body-format tag), and the browser addsContent-Length: 0. The runtime detects "no arguments" viarequest.body === null: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,extractBodyfinds no format tag and no content type, answersundefined, 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-jsv8 adapter, where every zero-arg call through the injected endpoint route 400'd.Reproduction
Same request with
body: nullanswers 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 treatContent-Length: 0as bodyless — or buffer the untagged/typeless body and skip theundefined-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-lengthis0(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/web2.0.0-rc.5 /solid-js2.0.0-rc.5.