Describe the bug
resolveFunctionId prefers the X-Server-Function-Id header over the id in the URL
(packages/web/server-functions/src/server.ts:844-850 on next). Caches, on the other hand,
key on the URL. For GET-declared functions — the ones GET(fn) exists to make cacheable — that
makes the header an unkeyed input to a cacheable response: a request can name one function
in the URL and a different one in the header, and the second function's response is what gets
stored under the first function's URL.
The header is trivially settable by any non-browser client (curl -H 'X-Server-Function-Id: …'),
and the same-origin gate is itself header-based, so it does not stand in the way.
Steps to reproduce
mkdir sf-repro && cd sf-repro && npm init -y
npm i @solidjs/web@2.0.0-rc.3 # pulls solid-js@2.0.0-rc.3
node --conditions=development repro.mjs # the dev condition only makes the error text readable
repro.mjs:
import { GET, configureServerFunctionsServer, createServerReference, handleServerFunctionRequest, registerServerReference } from "@solidjs/web/server-functions";
import { provideRequestEvent } from "@solidjs/web/storage";
configureServerFunctionsServer({ provideEvent: (event, run) => provideRequestEvent(event, run) });
GET(createServerReference(registerServerReference("pub-0", async () => ({ who: "public menu" }))));
GET(createServerReference(registerServerReference("priv-0", async () => ({ who: "private orders" }))));
const res = await handleServerFunctionRequest(
new Request("http://app.test/_server?id=pub-0", { // what a cache keys on
headers: { "Sec-Fetch-Site": "same-origin", "X-Server-Function-Id": "priv-0" }
})
);
console.log(res.status, await res.text());
Output on @solidjs/web@2.0.0-rc.3:
200 {"who":"private orders"}
So a shared cache in front of the app stores priv-0's response as the entry for
/_server?id=pub-0, and serves it to everyone who calls the public function afterwards. What
the substituted response actually contains depends on what the named function returns to an
unauthenticated caller, but the substitution itself is unconditional.
Expected behavior
For cacheable dispatch the URL should be authoritative: either ignore the header when the URL
carries an id, or drop the header as an id source for GET. Vary-ing on it instead would defeat
the caching the GET helper is for.
(Related: with the id in the query string rather than a path segment, a zone configured to
ignore query strings in the cache key — Bunny's IgnoreQueryStrings, CloudFront's legacy
"forward query strings: none" — collapses every server function in the app onto one cache entry.
That one is a design question rather than a bug, and I'm writing it up separately.)
Environment
|
|
@solidjs/web |
2.0.0-rc.3 (published) |
solid-js |
2.0.0-rc.3 |
| Node |
v24.19.0 |
| OS |
macOS (darwin 25.6.0) |
| Code references |
next @ f5939ef |
Describe the bug
resolveFunctionIdprefers theX-Server-Function-Idheader over the id in the URL(
packages/web/server-functions/src/server.ts:844-850onnext). Caches, on the other hand,key on the URL. For GET-declared functions — the ones
GET(fn)exists to make cacheable — thatmakes the header an unkeyed input to a cacheable response: a request can name one function
in the URL and a different one in the header, and the second function's response is what gets
stored under the first function's URL.
The header is trivially settable by any non-browser client (
curl -H 'X-Server-Function-Id: …'),and the same-origin gate is itself header-based, so it does not stand in the way.
Steps to reproduce
repro.mjs:Output on
@solidjs/web@2.0.0-rc.3:So a shared cache in front of the app stores
priv-0's response as the entry for/_server?id=pub-0, and serves it to everyone who calls the public function afterwards. Whatthe substituted response actually contains depends on what the named function returns to an
unauthenticated caller, but the substitution itself is unconditional.
Expected behavior
For cacheable dispatch the URL should be authoritative: either ignore the header when the URL
carries an id, or drop the header as an id source for GET.
Vary-ing on it instead would defeatthe caching the
GEThelper is for.(Related: with the id in the query string rather than a path segment, a zone configured to
ignore query strings in the cache key — Bunny's
IgnoreQueryStrings, CloudFront's legacy"forward query strings: none" — collapses every server function in the app onto one cache entry.
That one is a design question rather than a bug, and I'm writing it up separately.)
Environment
@solidjs/websolid-jsnext@f5939ef