From a26f2d730eb2619f1033a02df0d56a7afe1f3a6d Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 2 Oct 2026 01:48:01 -0700 Subject: [PATCH] fix(studio): Studio leaves cookies out of its requests only when served from loopback --- docs/packages/studio-server.mdx | 7 ++-- .../studio/src/utils/studioApiFetch.test.ts | 33 ++++++++++++++++++- packages/studio/src/utils/studioApiFetch.ts | 13 +++++--- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/docs/packages/studio-server.mdx b/docs/packages/studio-server.mdx index 8c21c5cd1f..640e199f27 100644 --- a/docs/packages/studio-server.mdx +++ b/docs/packages/studio-server.mdx @@ -35,8 +35,11 @@ export function mountStudioApi( } ``` -Studio sends its API requests without cookies, so they don't queue behind the -preview's media. A host that authenticates its mounted API with same-origin +When it is served from loopback (`localhost`, `127.0.0.1` or `[::1]`), Studio +sends its API requests without cookies, so they don't queue behind the +preview's media. On any other origin it sends them as usual, so remote and +proxied setups (Codespaces, tunnels, an access proxy) keep their cookies by +default. A loopback host that authenticates its mounted API with same-origin cookies sets `VITE_STUDIO_API_SAME_ORIGIN_CREDENTIALS=true`, at build time or at runtime in `window.__HF_STUDIO_ENV__`; it costs that host the connection speedup. diff --git a/packages/studio/src/utils/studioApiFetch.test.ts b/packages/studio/src/utils/studioApiFetch.test.ts index 64957e9cb3..e952f90886 100644 --- a/packages/studio/src/utils/studioApiFetch.test.ts +++ b/packages/studio/src/utils/studioApiFetch.test.ts @@ -28,7 +28,38 @@ describe("studioApiFetch", () => { }); }); - it("sends same-origin credentials for a host that opted out", async () => { + it.each([ + ["the CLI or Studio dev server", "http://localhost:5190/"], + ["Desktop's own server", "http://127.0.0.1:43117/"], + ["IPv6 loopback", "http://[::1]:8080/"], + ])("leaves cookies out when Studio is served from %s", async (_, page) => { + const fetchMock = vi.fn(async () => new Response("ok")); + vi.stubGlobal("fetch", fetchMock); + vi.stubGlobal("location", new URL(page)); + const { studioApiFetch } = await loadWithEnv({}); + + await studioApiFetch("/api/projects/demo/files/index.html"); + + expect(fetchMock).toHaveBeenCalledWith("/api/projects/demo/files/index.html", { + credentials: "omit", + }); + }); + + it("keeps same-origin cookies off loopback, where a proxy or tunnel may authenticate with them", async () => { + const fetchMock = vi.fn(async () => new Response("ok")); + vi.stubGlobal("fetch", fetchMock); + vi.stubGlobal("location", new URL("https://studio.example.com/editor")); + const { studioApiFetch } = await loadWithEnv({}); + + await studioApiFetch("/api/projects/demo/files/index.html"); + + expect(fetchMock).toHaveBeenCalledWith("/api/projects/demo/files/index.html", { + credentials: "same-origin", + }); + }); + + it("sends same-origin credentials on loopback for a host that opted out", async () => { + vi.stubGlobal("location", new URL("http://localhost:5190/")); const fetchMock = vi.fn(async () => new Response("ok")); vi.stubGlobal("fetch", fetchMock); const { studioApiFetch } = await loadWithEnv({ diff --git a/packages/studio/src/utils/studioApiFetch.ts b/packages/studio/src/utils/studioApiFetch.ts index 138882d531..88990ada22 100644 --- a/packages/studio/src/utils/studioApiFetch.ts +++ b/packages/studio/src/utils/studioApiFetch.ts @@ -1,8 +1,13 @@ import { STUDIO_API_SAME_ORIGIN_CREDENTIALS } from "../components/editor/manualEditingAvailability"; +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); + +function studioApiCredentials(): RequestCredentials { + if (STUDIO_API_SAME_ORIGIN_CREDENTIALS) return "same-origin"; + const host = globalThis.location?.hostname; + return !host || LOOPBACK_HOSTS.has(host) ? "omit" : "same-origin"; +} + export function studioApiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return globalThis.fetch(input, { - ...init, - credentials: STUDIO_API_SAME_ORIGIN_CREDENTIALS ? "same-origin" : "omit", - }); + return globalThis.fetch(input, { ...init, credentials: studioApiCredentials() }); }