Skip to content

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

Closed

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

  1. frenzzy commented on Aug 27, 2026

    @frenzzy
    ContributorAuthor

    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.

  2. ryansolid commented on Aug 27, 2026

    @ryansolid
    Member

    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).

  3. added a commit that references this issue on Aug 27, 2026
    79b96cf
  4. frenzzy commented on Aug 27, 2026

    @frenzzy
    ContributorAuthor

    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.

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