Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-planes-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Declare an explicit `Content-Type: text/plain; charset=utf-8` on seroval-stream server function responses (success and error paths) so intermediaries cannot content-sniff a type onto them
54 changes: 54 additions & 0 deletions packages/start/src/fns/handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,57 @@ describe("the configured server function error handler", () => {
expect(h3Event.res.headers.get("X-Error")).toBe("boom");
});
});

describe("seroval stream response headers", () => {
const callReturning = async (value: unknown) => {
const request = new Request("http://localhost/_server", {
method: "POST",
headers: { "X-Server-Id": "fn", "X-Server-Instance": "server-fn:1" },
});
const h3Event = { res: { headers: new Headers(), status: 200 } };
vi.mocked(getFetchEvent).mockReturnValue({
request,
response: { headers: { getSetCookie: () => [] } },
nativeEvent: h3Event,
locals: {},
} as unknown as FetchEvent);
vi.mocked(getServerFunction).mockReturnValue(() => value);
const { handleServerFunction } = await import("./handler.ts");
await handleServerFunction(h3Event as never);
return h3Event;
};

beforeEach(() => {
vi.clearAllMocks();
configuredErrorHandler.current = undefined;
});

it("sets an explicit content type on serialized results so intermediaries cannot sniff one", async () => {
const h3Event = await callReturning({ some: "value" });

expect(h3Event.res.headers.get("X-Start-Type")).toBe("0");
expect(h3Event.res.headers.get("content-type")).toBe("text/plain; charset=utf-8");
});

it("sets an explicit content type on serialized errors", async () => {
const request = new Request("http://localhost/_server", {
method: "POST",
headers: { "X-Server-Id": "fn", "X-Server-Instance": "server-fn:1" },
});
const h3Event = { res: { headers: new Headers(), status: 200 } };
vi.mocked(getFetchEvent).mockReturnValue({
request,
response: { headers: { getSetCookie: () => [] } },
nativeEvent: h3Event,
locals: {},
} as unknown as FetchEvent);
vi.mocked(getServerFunction).mockReturnValue(() => {
throw new Error("boom");
});
const { handleServerFunction } = await import("./handler.ts");
await handleServerFunction(h3Event as never);

expect(h3Event.res.headers.get("X-Start-Type")).toBe("0");
expect(h3Event.res.headers.get("content-type")).toBe("text/plain; charset=utf-8");
});
});
3 changes: 3 additions & 0 deletions packages/start/src/fns/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export async function handleServerFunction(h3Event: H3Event) {
h3Event.res.headers.set("content-type", "text/javascript");
return serializeToJSStream(instance, result);
}
// explicit type keeps intermediaries (e.g. Go proxies) from sniffing one onto the stream
h3Event.res.headers.set("content-type", "text/plain; charset=utf-8");
return serializeToJSONStream(result);
} catch (x) {
x = await applyServerFunctionErrorHandler(x);
Expand Down Expand Up @@ -165,6 +167,7 @@ export async function handleServerFunction(h3Event: H3Event) {
h3Event.res.headers.set("content-type", "text/javascript");
return serializeToJSStream(instance, x);
}
h3Event.res.headers.set("content-type", "text/plain; charset=utf-8");
return serializeToJSONStream(x);
}
return x;
Expand Down
Loading