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
feat(web): give scripted server-function calls their own data address (#3094)
One url served two answer shapes — codec encodings for the client
transport (keyed on the instance header), plain HTTP for everyone else —
and shared caches key on the url alone, so one caller kind's cached
answer could be replayed to the other. Scripted calls now go to
<endpoint>/data/<id>; the bare <endpoint>/<id> stays plain HTTP (a
reference's .url, rendered form actions, direct callers). The shape is
a function of the url, never a header. Transitional: the instance
header still summons the scripted shape at the bare address so loaded
tabs survive a deploy, with those answers forced no-store.
Co-authored-by: Cursor <cursoragent@cursor.com>
Scripted server-function calls now go to their own data address, `<endpoint>/data/<id>`, leaving the bare `<endpoint>/<id>` address to plain HTTP (#3094). The two caller kinds get differently shaped answers — codec encodings for the client transport, verbatim responses / form-convention handling for everyone else — and shared caches key on the URL, so a header-driven shape meant one caller kind's cached answer could be replayed to the other (a `GET`-declared function returning a raw `Response` with a public cache policy could serve its codec encoding to a browser navigation, or its raw body to the app's own transport). The answer's shape is now a function of the URL alone. A reference's `.url` and rendered action urls stay on the bare address; reconstructed callables splice the `data` segment in ahead of the id for their own calls. Transitional: the instance header still summons the scripted shape at the bare address so already-loaded tabs survive a server deploy, with those answers forced `no-store`.
Copy file name to clipboardExpand all lines: documentation/solid-2.0/10-server-functions.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,7 +44,7 @@ One architectural fact worth stating, because the two directive levels land on o
44
44
45
45
The package resolves to a client entry in the browser and a server entry elsewhere.
46
46
47
-
**Client:** `configureServerFunctionsClient({ endpoint?, codec?, fetch?, prepareRequest?, serializeArgs?, responseHandler? })` — call once in the client entry, only when deviating from the defaults (endpoint defaults to `/_server`; `codec` takes seroval plugin options and must match the server’s; `fetch` replaces the function the transport sends with — always called as `(address, init)` — for concerns the runtime has no opinion about: retries, telemetry, a test double, or pointing calls at a route of the app’s own, which the handler serves through the same `Request` it serves everything else with; `prepareRequest` is the transport middleware hook below; `responseHandler` is the integration seam server components install — see [RFC 11](11-server-components.md)). Compiled client output produces callables that POST to the call’s address — `<endpoint>/<id>`, the id in a path segment — with a per-call `X-Server-Function-Instance` id in the headers. **Argument encoding (updated since first draft):** arguments with a natural HTTP encoding (a lone string, FormData, File, Blob, ...) go as-is; everything else is sent as **plain JSON by default** — no serializer in the client bundle — and values JSON can’t carry faithfully (Dates, Maps, Sets, typed arrays, cycles) **throw with a directed message** unless you opt in once via `enableRichArguments()` from `@solidjs/web/server-functions/rich-args`, which installs the codec’s write half (~5 KB gz) as `serializeArgs` — importing the entry is the opt-in at the module-graph level, so the serializer ships only when the app asks for it. _Results_ are unaffected — they always travel through the codec, whose decode half the client carries regardless. Async returns (promises, streams) settle over the open connection via length-prefixed chunk framing. (A `@solidjs/web/serialization` subpath exists; most of it is integration-facing plumbing — the bridge exposing the runtime’s serializer machinery for the runtime’s own entries and for integrations building transports — exempt from the 2.0 stability guarantee and subject to change. The one application-facing part is plugin _authoring_: `createPlugin` and `OpaqueReference` are re-exported there from the runtime’s own seroval instance, and custom plugins for the `codec` option must be built from that import — a plugin built against your own `seroval` dependency edge would not fail the build, it would emit nodes the other end of the wire can’t interpret (the version-pinning lesson of solid-start #1474). Application and router code authors plugins there and feeds them to `codec`; everything else on the subpath it should leave alone.)
47
+
**Client:** `configureServerFunctionsClient({ endpoint?, codec?, fetch?, prepareRequest?, serializeArgs?, responseHandler? })` — call once in the client entry, only when deviating from the defaults (endpoint defaults to `/_server`; `codec` takes seroval plugin options and must match the server’s; `fetch` replaces the function the transport sends with — always called as `(address, init)` — for concerns the runtime has no opinion about: retries, telemetry, a test double, or pointing calls at a route of the app’s own, which the handler serves through the same `Request` it serves everything else with; `prepareRequest` is the transport middleware hook below; `responseHandler` is the integration seam server components install — see [RFC 11](11-server-components.md)). Compiled client output produces callables that POST to the call’s **data address** — `<endpoint>/data/<id>`, the id in a path segment — with a per-call `X-Server-Function-Instance` id in the headers. The data address is the scripted transport’s own path, where answers are the codec’s; the bare `<endpoint>/<id>` address (a reference’s `.url`, what renders into form actions) answers plain HTTP. Two paths because the two caller kinds get differently shaped answers and shared caches key on the URL: with one shape per path, a cached answer can only ever be replayed to the caller kind it was made for. **Argument encoding (updated since first draft):** arguments with a natural HTTP encoding (a lone string, FormData, File, Blob, ...) go as-is; everything else is sent as **plain JSON by default** — no serializer in the client bundle — and values JSON can’t carry faithfully (Dates, Maps, Sets, typed arrays, cycles) **throw with a directed message** unless you opt in once via `enableRichArguments()` from `@solidjs/web/server-functions/rich-args`, which installs the codec’s write half (~5 KB gz) as `serializeArgs` — importing the entry is the opt-in at the module-graph level, so the serializer ships only when the app asks for it. _Results_ are unaffected — they always travel through the codec, whose decode half the client carries regardless. Async returns (promises, streams) settle over the open connection via length-prefixed chunk framing. (A `@solidjs/web/serialization` subpath exists; most of it is integration-facing plumbing — the bridge exposing the runtime’s serializer machinery for the runtime’s own entries and for integrations building transports — exempt from the 2.0 stability guarantee and subject to change. The one application-facing part is plugin _authoring_: `createPlugin` and `OpaqueReference` are re-exported there from the runtime’s own seroval instance, and custom plugins for the `codec` option must be built from that import — a plugin built against your own `seroval` dependency edge would not fail the build, it would emit nodes the other end of the wire can’t interpret (the version-pinning lesson of solid-start #1474). Application and router code authors plugins there and feeds them to `codec`; everything else on the subpath it should leave alone.)
48
48
49
49
**Server:**`configureServerFunctionsServer({ endpoint?, codec?, provideEvent?, wrapInvocation?, collectFlightData?, transformResult?, transformDirectResult? })` plus the web-standard HTTP handler:
50
50
@@ -118,7 +118,7 @@ The protocol folds integration data (typically revalidated route data) into a mu
118
118
119
119
### No-JS and progressive enhancement
120
120
121
-
A reference’s `.url` serves as a form `action`, and action urls are **self-describing** (`<endpoint>/<id>?args=...`): an integration can reconstruct a callable from a server-rendered action url alone, with bound arguments kept in the query string where the server reads them for natural-encoding bodies. `serverFunctionUrl(id, boundArgs?)` and `parseServerFunctionUrl(url)` are the two halves of that scheme for integrations composing action urls the runtime did not render. The absence of the `X-Server-Function-Instance` header marks an unscripted call (a form submit or direct HTTP); arguments are parsed from the query string or FormData by content-type sniffing — a no-JS form post decodes to a lone `FormData` argument, and a read whose query is not an argument encoding hands that query over as a lone `URLSearchParams`, which is what a `method="get"` form submits (the browser replaces the action url’s query with its fields, so only an address in the path survives one). Which reading applies is decided by the url alone, never by a header, so a cache cannot be made to store one reading and serve it for the other; `args` is reserved on the query, and a value under it that is not an argument array answers 400. What a GET submit renders is the function’s to shape — the no-JS redirect convention is a form-post one. The `handleNoJS` handler hook builds the response for these calls (default: the normal serialized response).
121
+
A reference’s `.url` serves as a form `action`, and action urls are **self-describing** (`<endpoint>/<id>?args=...`): an integration can reconstruct a callable from a server-rendered action url alone, with bound arguments kept in the query string where the server reads them for natural-encoding bodies (the callable’s own calls go to the rendered address’s data-address sibling — same mount, same query). `serverFunctionUrl(id, boundArgs?)` and `parseServerFunctionUrl(url)` are the two halves of that scheme for integrations composing action urls the runtime did not render. The bare address marks an unscripted call (a form submit or direct HTTP) — the shape of the answer is the address’s, never a header’s (the `X-Server-Function-Instance` header still signals scripted-ness at the bare address as a transitional courtesy to pre-split clients, with the answer forced `no-store`); arguments are parsed from the query string or FormData by content-type sniffing — a no-JS form post decodes to a lone `FormData` argument, and a read whose query is not an argument encoding hands that query over as a lone `URLSearchParams`, which is what a `method="get"` form submits (the browser replaces the action url’s query with its fields, so only an address in the path survives one). Which reading applies is decided by the url alone, never by a header, so a cache cannot be made to store one reading and serve it for the other; `args` is reserved on the query, and a value under it that is not an argument array answers 400. What a GET submit renders is the function’s to shape — the no-JS redirect convention is a form-post one. The `handleNoJS` handler hook builds the response for these calls (default: the normal serialized response).
122
122
123
123
The full unscripted flow (flash cookie → redirect → SSR-seeded submission state) has a settled ownership chain:
Calls go over HTTP GET with arguments codec-encoded in the query string of the call’s address — cacheable by HTTP infrastructure (the varying instance header doesn’t break caching; caches key on URL unless `Vary` says otherwise). Arguments too long for a url dispatch over POST instead, which costs the cache entry rather than meeting whichever proxy in the chain draws the line at a 414. Cache headers flow through the handler’s existing header forwarding: `respond(data, { headers: { "cache-control": "max-age=60" } })`. Server-side, the wrapper is identity-flavored — SSR calls stay in-process. Because function-level directives round-trip wrapper calls (above), this needs **no compiler involvement**.
154
+
Calls go over HTTP GET with arguments codec-encoded in the query string of the call’s data address — cacheable by HTTP infrastructure (the varying instance header doesn’t break caching; caches key on URL unless `Vary` says otherwise, and the data address serves the codec shape to every caller, so what a cache stores there is right for anyone who reads it). Arguments too long for a url dispatch over POST instead, which costs the cache entry rather than meeting whichever proxy in the chain draws the line at a 414. Cache headers flow through the handler’s existing header forwarding: `respond(data, { headers: { "cache-control": "max-age=60" } })`. Server-side, the wrapper is identity-flavored — SSR calls stay in-process. Because function-level directives round-trip wrapper calls (above), this needs **no compiler involvement**.
155
155
156
156
Under the sugar sits a symbol-branded metadata channel (`Symbol.for`, surviving duplicated module instances — the same trick as the `ResponseEnvelope` brand), populated on both proxies and read through typed accessors. `withMeta(fn, meta)` is its public write path — it exists because `prepareRequest`’s `meta` parameter was otherwise unreachable for user declarations — and `GET` is sugar over the same write:
0 commit comments