diff --git a/documentation/solid-2.0/10-server-functions.md b/documentation/solid-2.0/10-server-functions.md
index 16cf20b1a4..643f3e2c97 100644
--- a/documentation/solid-2.0/10-server-functions.md
+++ b/documentation/solid-2.0/10-server-functions.md
@@ -165,7 +165,7 @@ export const getUser = GET(async (id: string) => {
});
```
-Calls go over HTTP GET with arguments codec-encoded in the query string of the call’s data address — cacheable by HTTP infrastructure (a read sends no header of its own, so caches and `` match it on the URL alone, 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" } })`. `serverFunctionUrl(fn, ...args)` renders that url as a value — for ``, a prefetch, a service-worker warm, or a fetch by hand (decode the answer with `decodeResponse`) — built the way the transport builds it, from the reference (the only thing that knows the call is a GET). It refuses, rather than answering with an address nothing would request, for a reference on the default transport (a POST is not described by its url; `invoke(fn, { priority: "low" }, ...args)` starts such a call early), for arguments that need the codec, and for a url past the POST-fallback length. For a `live(GET(fn))` reference it renders the live address (`/live/?args=…`) — the url that reference's own call requests, a standing event stream to fetch by hand (`curl -N`), not to preload or prefetch (that would open a stream nothing reads; warm a live address by calling the reference, whose readers share one connection); the one-shot url is the inner `GET(fn)`'s. 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**.
+Calls go over HTTP GET with arguments codec-encoded in the query string of the call’s data address — cacheable by HTTP infrastructure (a read sends no header of its own, so caches and `` match it on the URL alone, 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" } })`. Those headers belong to the call’s own data address: when a page calls the function in-process during SSR, the envelope is unwrapped and only `Set-Cookie` reaches the page’s response (#3910) — a read’s `Cache-Control` never becomes the page’s. A header written through `getRequestEvent().response.headers` is not unwrapped this way; the in-process call runs under a derived event marked `serverOnly`, which tells the two legs apart. `serverFunctionUrl(fn, ...args)` renders that url as a value — for ``, a prefetch, a service-worker warm, or a fetch by hand (decode the answer with `decodeResponse`) — built the way the transport builds it, from the reference (the only thing that knows the call is a GET). It refuses, rather than answering with an address nothing would request, for a reference on the default transport (a POST is not described by its url; `invoke(fn, { priority: "low" }, ...args)` starts such a call early), for arguments that need the codec, and for a url past the POST-fallback length. For a `live(GET(fn))` reference it renders the live address (`/live/?args=…`) — the url that reference's own call requests, a standing event stream to fetch by hand (`curl -N`), not to preload or prefetch (that would open a stream nothing reads; warm a live address by calling the reference, whose readers share one connection); the one-shot url is the inner `GET(fn)`'s. 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**.
Conditional reads belong here too, and to the **browser**: set `ETag`/`Cache-Control` on a GET-declared read and the browser owns the conditional exchange — it sends `If-None-Match`, receives the 304, and replays its cached 200 without the caller ever seeing a 304. Answering a _scripted_ call with a hand-rolled `respond(undefined, { status: 304 })` answers a question the transport never asked — it sends no conditional headers — so the bodiless answer resolves the call to `undefined`, which reads as data loss. The dev build warns when a scripted call is answered with a 304 (#3101).