fix(studio): Studio leaves cookies out of its requests only when served from loopback

This commit is contained in:
Miguel Angel Simon Sierra
2026-10-02 03:39:01 -07:00
parent 411c7c5bda
commit a26f2d730e
3 changed files with 46 additions and 7 deletions
+5 -2
View File
@@ -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.
@@ -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({
+9 -4
View File
@@ -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<Response> {
return globalThis.fetch(input, {
...init,
credentials: STUDIO_API_SAME_ORIGIN_CREDENTIALS ? "same-origin" : "omit",
});
return globalThis.fetch(input, { ...init, credentials: studioApiCredentials() });
}