Duplicates
Latest version
Current behavior π―
(Rewritten for SolidStart v2 as requested β the original v1 report is in the edit history.)
In v2, seroval-stream server-function responses are sent without a Content-Type header. packages/start/src/fns/handler.ts sets one only in the js mode branch:
h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval);
if (import.meta.env.SEROVAL_MODE === "js") {
h3Event.res.headers.set("content-type", "text/javascript");
return serializeToJSStream(instance, result);
}
return serializeToJSONStream(result); // <-- no content-type (also in the catch branch)
Reverse proxies built on Go's net/http (kamal-proxy, Traefik-style middlewares, any httputil.ReverseProxy where a handler writes the response) content-sniff bodies that lack a Content-Type and inject one. The seroval chunk stream ;0x000048f1;{...} sniffs as text/plain; charset=utf-8, so clients and intermediaries downstream see a mislabeled response.
SolidStart's own v2 client is not broken by this β extractBody dispatches on X-Start-Type first, which is the right design. π But the missing header still has real costs:
- Anything that is not the Start client sees
text/plain β logging/observability tooling, caches and CDNs applying content-type rules (e.g. compression or transformation filters keyed on text/*), and browser devtools previews.
- The response is at the mercy of whatever an intermediary guesses. A different sniffer could guess differently per body prefix, so behavior becomes deployment-dependent.
- HTTP semantics (RFC 9110 Β§8.3): a sender that knows the content's nature ought to declare it; omitting it is what invites sniffing in the first place.
- Historical evidence of the risk: in v1 (1.3.2) the same missing header + a Go proxy caused every client-side navigation to hang silently, because that client dispatched on Content-Type before
x-serialized. v2's dispatch order fixes the hang, but the root omission carried over.
Expected behavior π€
The server always sets an explicit Content-Type on seroval streams, in both the success and error paths β mirroring what js mode already does with text/javascript. application/octet-stream (or a dedicated type like application/x-seroval-stream) works; anything explicit prevents sniffing. One-line change next to the existing BODY_FORMAT_KEY header set.
Steps to reproduce πΉ
Steps:
- Any SolidStart 2.0.0 app with a
"use server" function called from the client.
- Deploy behind a minimal Go reverse proxy (
httputil.ReverseProxy with any body-touching handler, or kamal-proxy as-is).
- Call the server function; inspect the response headers in devtools.
- Observe injected
content-type: text/plain; charset=utf-8 on the seroval stream (direct hit to the origin shows no Content-Type at all).
Context π¦
We run SolidStart behind kamal-proxy (Kamal is Rails' default deploy tooling, so this proxy is common). On v1 this exact omission produced a hard-to-debug production incident (silent navigation hang, no console errors); we work around it today with middleware that sets Content-Type: application/octet-stream on /_server responses that lack one. Declaring the type at the source would remove the workaround and make the transport deployment-independent. Happy to send a PR if the approach is acceptable.
Your environment π
System:
OS: Linux (Docker) / macOS 15 local
Binaries:
Node: 24.x
pnpm: 10.x
npmPackages:
@solidjs/start: 2.0.0 (source-reviewed; production repro observed on 1.3.2 behind the same proxy)
solid-js: 1.9.x
Proxy: kamal-proxy (Go net/http), TLS terminated at proxy
Duplicates
Latest version
Current behavior π―
(Rewritten for SolidStart v2 as requested β the original v1 report is in the edit history.)
In v2, seroval-stream server-function responses are sent without a
Content-Typeheader.packages/start/src/fns/handler.tssets one only in thejsmode branch:Reverse proxies built on Go's
net/http(kamal-proxy, Traefik-style middlewares, anyhttputil.ReverseProxywhere a handler writes the response) content-sniff bodies that lack a Content-Type and inject one. The seroval chunk stream;0x000048f1;{...}sniffs astext/plain; charset=utf-8, so clients and intermediaries downstream see a mislabeled response.SolidStart's own v2 client is not broken by this β
extractBodydispatches onX-Start-Typefirst, which is the right design. π But the missing header still has real costs:text/plainβ logging/observability tooling, caches and CDNs applying content-type rules (e.g. compression or transformation filters keyed ontext/*), and browser devtools previews.x-serialized. v2's dispatch order fixes the hang, but the root omission carried over.Expected behavior π€
The server always sets an explicit Content-Type on seroval streams, in both the success and error paths β mirroring what
jsmode already does withtext/javascript.application/octet-stream(or a dedicated type likeapplication/x-seroval-stream) works; anything explicit prevents sniffing. One-line change next to the existingBODY_FORMAT_KEYheader set.Steps to reproduce πΉ
Steps:
"use server"function called from the client.httputil.ReverseProxywith any body-touching handler, or kamal-proxy as-is).content-type: text/plain; charset=utf-8on the seroval stream (direct hit to the origin shows no Content-Type at all).Context π¦
We run SolidStart behind kamal-proxy (Kamal is Rails' default deploy tooling, so this proxy is common). On v1 this exact omission produced a hard-to-debug production incident (silent navigation hang, no console errors); we work around it today with middleware that sets
Content-Type: application/octet-streamon/_serverresponses that lack one. Declaring the type at the source would remove the workaround and make the transport deployment-independent. Happy to send a PR if the approach is acceptable.Your environment π