You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
X-Server-Function-Id overrides the id in the URL, making it an unkeyed input to a cacheable response #3070
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"}))));constres=awaithandleServerFunctionRequest(newRequest("http://app.test/_server?id=pub-0",{// what a cache keys onheaders: {"Sec-Fetch-Site": "same-origin","X-Server-Function-Id": "priv-0"}}));console.log(res.status,awaitres.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.)
Design context: this came out of a broader look at how server function calls are addressed — #3072. That discussion proposes moving the id into a path segment (which would also resolve this issue's root cause for the URL-vs-header case, and is what makes no-JS method="get" forms possible at all), but this issue is independent of it and can be fixed on its own.
Keeping this one open but holding the fix: the addressing discussion in #3072 resolves it by design — with the function id in the URL path there is no header to override, so the id becomes a keyed input to the cache automatically. Patching header precedence now would just be churn on a shape we intend to replace.
Interim exposure is narrow after #3069/#3071 landed: the override only matters for cacheable responses, POST responses are no-store by default now, and a poisoning attempt would need direct edge access plus two GET-declared functions to swap between. If #3072 stalls for any reason we'll fix the precedence directly (URL id wins, header only as fallback when the URL carries none).
Resolved by 79b96cf (#3076) — and in the stronger form than the fallback plan above: the id now travels only in the path and X-Server-Function-Id is gone from the wire and from the package's exports, so there is no second source for a cache to disagree with.
Verified on next: POST /_server/<B> carrying X-Server-Function-Id: <A> executes B, and /_server?id=<A> answers 404. Both are pinned in server-functions-addressing.spec.tsx.
Closing on that basis — reopen if you'd rather keep it open until the change publishes.
Activity
frenzzy commentedon Aug 27, 2026
Design context: this came out of a broader look at how server function calls are addressed — #3072. That discussion proposes moving the id into a path segment (which would also resolve this issue's root cause for the URL-vs-header case, and is what makes no-JS
method="get"forms possible at all), but this issue is independent of it and can be fixed on its own.ryansolid commentedon Aug 27, 2026
Keeping this one open but holding the fix: the addressing discussion in #3072 resolves it by design — with the function id in the URL path there is no header to override, so the id becomes a keyed input to the cache automatically. Patching header precedence now would just be churn on a shape we intend to replace.
Interim exposure is narrow after #3069/#3071 landed: the override only matters for cacheable responses, POST responses are
no-storeby default now, and a poisoning attempt would need direct edge access plus twoGET-declared functions to swap between. If #3072 stalls for any reason we'll fix the precedence directly (URL id wins, header only as fallback when the URL carries none).feat(web): address server function calls by path
feat(web): address server function calls by path
feat(web): address server function calls by path
feat(web): address server function calls by path
feat(web): address server function calls by path
feat(web): address server function calls by path (#3076)
frenzzy commentedon Aug 27, 2026
Resolved by 79b96cf (#3076) — and in the stronger form than the fallback plan above: the id now travels only in the path and
X-Server-Function-Idis gone from the wire and from the package's exports, so there is no second source for a cache to disagree with.Verified on
next:POST /_server/<B>carryingX-Server-Function-Id: <A>executes B, and/_server?id=<A>answers 404. Both are pinned inserver-functions-addressing.spec.tsx.Closing on that basis — reopen if you'd rather keep it open until the change publishes.