Repository navigation
dev HMR still drops GET grants for query()-wrapped server functions (405 until next SSR render) — residual after #3353 #3564
Description
Versions
@solidjs/web@2.0.0-rc.9@solidjs/vite-plugin@3.0.0-next.44@solidjs/router@2.0.0-next.26solid-js@2.0.0-rc.9
Summary
On rc.9 — which already includes #3353 (the fix for #3346) — any dev edit that triggers an SSR program reload makes every query()-declared server function answer 405 Method Not Allowed (Allow: POST) until the next full document SSR render. Subsequent client navigations keep failing with Server function call failed with status 405.
This is the same user-visible symptom as #3346, but a different mechanism than the one #3353 fixed, so #3353 does not close it.
Reproduction
query()-wrapped "use server" reads (routed to /_server/data/<id>?args=…; ids are stable across reloads — hash(root-relative path)):
GET /— the SSR render runs theGET()declaration, recording the grant.GET /_server/data/getYearsServer-f84b56ac?args=[]withSec-Fetch-Site: same-origin→ 200.- Edit any file in the SSR graph — a server module or a pure client component (both cause
(ssr) program reload). HMR applies. - Same GET → 405,
Allow: POST, bodyMethod not allowed for server function: getYearsServer-f84b56ac. GET /(full page render) → the declaration re-runs; GET is 200 again.
The function id in the URL is byte-identical across the reload, so this is not URL/version skew. A cold dev server also answers 405 before any page has rendered, for the same reason.
Note the SSR program reload fires frequently and sometimes spuriously in dev (it is emitted alongside (client) hmr update /src/index.css?direct), so the 405 appears "randomly" during normal work.
Mechanism
- The
GETgrant lives in the SSR-environmentMETHODSmap (server-functions/src/server.ts). It is created only when the declaring site runs in the SSR environment — for the router pattern, that is thequery(fn, name)call site (the router wraps the reference withGET(fn)at query-creation). registerServerFunction(introduced by fix(web): revoke GET grants on rebind; never fold single-flight into reads #3150) performsMETHODS.delete(id)whenever the id's callback identity changes — which is guaranteed on every HMR re-evaluation of a"use server"module.- fix(web): revoke GET grants on rebind; never fold single-flight into reads #3150 relies on "module order makes this self-healing": the module that re-registers an id then re-runs
GET()in sequence. With the router pattern, the declaration lives in a different module (query()is called from the app's data layer, which imports the server module). The HMR path re-imports only the server module, not thequery()call site, so the revoked grant is never re-armed. - fix(web): keep the server-function registry on globalThis across runtime re-evaluation #3353 is not involved here: in rc.9 the three maps already ride
globalThisviaprocessState(...), so there is no duplicate-instance divergence; the revocation in the shared map is the operative cause.
Instrumenting the installed dev bundle shows the sequence: the SSR program reload re-registers the id with a fresh callback (same path-derived id), declaresRead(id) flips to false, and dispatch answers 405 until a later document render re-runs the query()/GET() site.
Expected
A declaration scoped to a stable, path-derived id should survive benign dev re-registration of that same id. Dropping it on identity churn is correct in production (rebinding protection, #3129), but in dev it makes the router query() pattern unusable after any edit.
Proposed fix
In server-functions/src/server.ts, carry the grant across an id rebind in dev only:
export function registerServerFunction(id, callback) {
provideRPC();
if (REGISTRATIONS.get(id) !== callback) {
if (DEV && METHODS.has(id)) METHODS.set(id, callback); // HMR: id ↔ source is stable
else METHODS.delete(id); // production: keep revocation
}
REGISTRATIONS.set(id, callback);
return callback;
}Because compiler ids are path-derived (<name>-<xxhash32(path)>[-ordinal], see #3120), the same id in dev denotes the same declaration site, so the carry-over does not weaken #3129's concern (a different function never inherits the grant in production, and in dev a same-id function is a re-evaluation of the same declaration).
An alternative would be to have the dev middleware re-evaluate the declaration module, but the middleware has no way to know which module declared the read.
Verification
Applied as a dev-only patch to the installed rc.9 bundle:
| Step | Before | After |
|---|---|---|
| GET after SSR render | 200 | 200 |
| Edit a client component → HMR program reload | 405 (Allow: POST) |
200 |
| GET again | 405 | 200 |
A regression test modeled on #3353's server-functions-registry-reload.spec.tsx can pin it: boot, declare via getServerFunctionRPC().GET, re-register the same id with a fresh callback (simulating HMR), assert declaresRead(id) remains true and dispatch answers 200.
Related: #3346 (same symptom), #3353 (different cause, already fixed), #3150 / #3129 / #3237 (grant lifetime and rebind revocation).
Activity
fix(web): carry a live GET grant across a dev-only rebind (solidjs#3564)
fix(web): carry a dev rebind's GET grant as dispatch-only; origin gat…
fix(web): carry a live GET grant across a dev-only rebind (#3564) (#3566
ryansolid commentedon Sep 22, 2026