Skip to content

X-Server-Function-Id overrides the id in the URL, making it an unkeyed input to a cacheable response #3070

Description

@frenzzy

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

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