From b5319f5f7e0a2c7a6eaa50e0a932c88a630b5c25 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 26 Nov 2025 18:38:37 +0100 Subject: [PATCH 1/4] redirect Response with multiple Set-Cookie headers --- .changeset/chilled-turkeys-leave.md | 5 +++++ packages/start/src/server/handler.ts | 31 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 .changeset/chilled-turkeys-leave.md diff --git a/.changeset/chilled-turkeys-leave.md b/.changeset/chilled-turkeys-leave.md new file mode 100644 index 000000000..836cbd309 --- /dev/null +++ b/.changeset/chilled-turkeys-leave.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +redirect Response with multiple Set-Cookie headers diff --git a/packages/start/src/server/handler.ts b/packages/start/src/server/handler.ts index cdcacc600..080eb1c80 100644 --- a/packages/start/src/server/handler.ts +++ b/packages/start/src/server/handler.ts @@ -1,9 +1,9 @@ -import middleware from "solid-start:middleware"; import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3"; import { join } from "pathe"; import type { JSX } from "solid-js"; import { sharedConfig } from "solid-js"; import { getRequestEvent, renderToStream, renderToString } from "solid-js/web"; +import middleware from "solid-start:middleware"; import { createRoutes } from "../router.tsx"; import { decorateHandler, decorateMiddleware } from "./fetchEvent.ts"; @@ -11,10 +11,10 @@ import { getSsrManifest } from "./manifest/ssr-manifest.ts"; import { matchAPIRoute } from "./routes.ts"; import { handleServerFunction } from "./server-functions-handler.ts"; import type { - APIEvent, - FetchEvent, - HandlerOptions, - PageEvent, + APIEvent, + FetchEvent, + HandlerOptions, + PageEvent, } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -235,15 +235,32 @@ function produceResponseWithEventHeaders(res: Response) { // Response.redirect returns an immutable value, so we clone on any redirect just in case if(300 <= res.status && res.status < 400) { + const cookies = res.headers.getSetCookie?.() ?? []; + const headers = new Headers(); + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== 'set-cookie') { + headers.set(key, value); + } + }); + for (const cookie of cookies) { + headers.append('Set-Cookie', cookie); + } ret = new Response(res.body, { status: res.status, statusText: res.statusText, - headers: Object.fromEntries(res.headers.entries()) + headers, }); } + const eventCookies = event.response.headers.getSetCookie?.() ?? []; + for (const cookie of eventCookies) { + ret.headers.append('Set-Cookie', cookie); + } + for(const [name, value] of event.response.headers) { - ret.headers.set(name, value); + if (name.toLowerCase() !== 'set-cookie') { + ret.headers.set(name, value); + } } return ret From 8e0beea06750ea92e822cc0ce7006b01262ba0fd Mon Sep 17 00:00:00 2001 From: Birk Skyum <74932975+birkskyum@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:52:29 +0100 Subject: [PATCH 2/4] format --- packages/start/src/server/handler.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/start/src/server/handler.ts b/packages/start/src/server/handler.ts index 080eb1c80..a794f1697 100644 --- a/packages/start/src/server/handler.ts +++ b/packages/start/src/server/handler.ts @@ -1,9 +1,9 @@ +import middleware from "solid-start:middleware"; import { defineHandler, getCookie, H3, type H3Event, redirect, setCookie } from "h3"; import { join } from "pathe"; import type { JSX } from "solid-js"; import { sharedConfig } from "solid-js"; import { getRequestEvent, renderToStream, renderToString } from "solid-js/web"; -import middleware from "solid-start:middleware"; import { createRoutes } from "../router.tsx"; import { decorateHandler, decorateMiddleware } from "./fetchEvent.ts"; @@ -11,10 +11,10 @@ import { getSsrManifest } from "./manifest/ssr-manifest.ts"; import { matchAPIRoute } from "./routes.ts"; import { handleServerFunction } from "./server-functions-handler.ts"; import type { - APIEvent, - FetchEvent, - HandlerOptions, - PageEvent, + APIEvent, + FetchEvent, + HandlerOptions, + PageEvent, } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; From a603f8296c2e35e884ade94f9ae88769c4d79a8c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 26 Nov 2025 18:53:46 +0100 Subject: [PATCH 3/4] changset --- .changeset/chilled-turkeys-leave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilled-turkeys-leave.md b/.changeset/chilled-turkeys-leave.md index 836cbd309..ba8aaa316 100644 --- a/.changeset/chilled-turkeys-leave.md +++ b/.changeset/chilled-turkeys-leave.md @@ -2,4 +2,4 @@ "@solidjs/start": patch --- -redirect Response with multiple Set-Cookie headers +Fix multiple Set-Cookie headers being lost on redirect responses From 4340ce6d77a11b56cb5fb350cb4dbf56994394d2 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 26 Nov 2025 19:39:04 +0100 Subject: [PATCH 4/4] add tests --- apps/tests/src/e2e/api-call.test.ts | 25 ++++++++++++++++++- .../routes/api/multi-set-cookie-redirect.ts | 17 +++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 apps/tests/src/routes/api/multi-set-cookie-redirect.ts diff --git a/apps/tests/src/e2e/api-call.test.ts b/apps/tests/src/e2e/api-call.test.ts index 07dbd2a82..29a354b7e 100644 --- a/apps/tests/src/e2e/api-call.test.ts +++ b/apps/tests/src/e2e/api-call.test.ts @@ -18,5 +18,28 @@ test.describe("api calls", () => { expect(redirectResp.headers.get("x-event-header")).toBe("value"); expect(redirectResp.headers.get("x-return-header")).toBe("value"); expect(redirectResp.headers.get("x-shared-header")).toBe("event"); - }) + }); + + test("should preserve multiple Set-Cookie headers on redirect (RFC 6265)", async () => { + + const response = await fetch("http://localhost:3000/api/multi-set-cookie-redirect", { + redirect: "manual" + }); + + expect(response.status).toBe(302); + + // Use getSetCookie() to retrieve all Set-Cookie headers as an array + const cookies = response.headers.getSetCookie(); + + // We expect 3 cookies: + // 1. session=abc123 (from response headers) + // 2. csrf=xyz789 (from response headers) + // 3. event_cookie=from_event (from event.response headers via setHeader) + expect(cookies.length).toBe(3); + + const cookieValues = cookies.join("; "); + expect(cookieValues).toContain("session=abc123"); + expect(cookieValues).toContain("csrf=xyz789"); + expect(cookieValues).toContain("event_cookie=from_event"); + }); }); diff --git a/apps/tests/src/routes/api/multi-set-cookie-redirect.ts b/apps/tests/src/routes/api/multi-set-cookie-redirect.ts new file mode 100644 index 000000000..1877c1677 --- /dev/null +++ b/apps/tests/src/routes/api/multi-set-cookie-redirect.ts @@ -0,0 +1,17 @@ +import { setHeader } from "@solidjs/start/http"; + +export async function GET() { + // Set a cookie via the event headers (this tests merging event headers) + setHeader("Set-Cookie", "event_cookie=from_event; Path=/"); + + // This tests cloning redirect responses with multiple cookies + const headers = new Headers(); + headers.append("Location", "http://localhost:3000/"); + headers.append("Set-Cookie", "session=abc123; Path=/; HttpOnly"); + headers.append("Set-Cookie", "csrf=xyz789; Path=/"); + + return new Response(null, { + status: 302, + headers + }); +}