Files
webstudio/apps/builder/app/shared/fetch.client.ts
Oleg Isonen d259a6d970 fix: Prevent caching builder user data (#5761)
**Summary**

Migrates Builder bootstrap data loading from the legacy
`/rest/data/:projectId` endpoint to `build.loadData` tRPC, while keeping
the old REST route as a no-store compatibility shim.

Hardens authenticated Builder/dashboard responses by explicitly sending:

- `Cache-Control: private, no-store, max-age=0`
- `Pragma: no-cache`
- `Expires: 0`
- `Vary: Cookie`

**Details**

- Added `build.loadData` tRPC query for full Builder state.
- Switched Builder client bootstrap loading to tRPC.
- Added shared cache-control helpers for private no-store responses.
- Made tRPC responses private/no-store by default unless anonymous and
explicitly public-cacheable.
- Added no-store headers to
Builder/dashboard/auth/resource/asset-related responses.
- Updated redirect handling to preserve headers while enforcing
no-store.
- Prevented stale dashboard account data from flashing by applying
user-scoped loader data before paint and clearing stores on teardown.
- Added tests for cache headers, tRPC response metadata, and no-store
redirects.

**Validation**

- `pnpm --filter @webstudio-is/builder test --
app/services/cache-control.server.test.ts
app/services/trpc-response-meta.server.test.ts
app/services/no-store-redirect.test.ts app/routes/rest.assets.test.ts
app/shared/sync/project-queue.test.ts app/dashboard/dashboard.test.ts`
- `pnpm exec oxlint ...` on touched files
- `git diff --check`

`pnpm --filter @webstudio-is/builder typecheck` still fails on existing
unrelated Tailwind/css-engine errors.
2026-05-16 20:22:03 +02:00

70 lines
2.1 KiB
TypeScript

import { toast } from "@webstudio-is/design-system";
import {
apiClientHeader,
apiClientVersionHeader,
getApiCompatibilityPayload,
} from "@webstudio-is/trpc-interface/api-compatibility";
import { csrfToken } from "./csrf.client";
import { $authToken } from "./nano-states";
import { publicStaticEnv } from "~/env/env.static";
/**
* To avoid fetch interception from the canvas, i.e., `globalThis.fetch = () => console.log('INTERCEPTED');`,
*/
const _fetch = globalThis.fetch;
/**
* To avoid fetch interception from the canvas, i.e., `globalThis.fetch = () => console.log('INTERCEPTED');`,
* To add csrf token to the headers.
*/
export const fetch: typeof globalThis.fetch = async (
requestInfo,
requestInit
) => {
if (csrfToken === undefined) {
toast.error("CSRF token is not set.");
throw new Error("CSRF token is not set.");
}
const headers = new Headers(requestInit?.headers);
headers.set("X-CSRF-Token", csrfToken);
headers.set(apiClientHeader, "browser");
headers.set(apiClientVersionHeader, publicStaticEnv.VERSION);
const authToken = $authToken.get();
// Do not override the existing x-auth-token header.
// As some mutations are queue based and they need to be authenticated with the same token as in the queue.
if (authToken !== undefined && headers.get("x-auth-token") === null) {
headers.set("x-auth-token", authToken);
}
const modifiedInit: RequestInit = {
...requestInit,
cache: requestInit?.cache ?? "no-store",
headers,
};
const response = await _fetch(requestInfo, modifiedInit);
if (response.ok === false) {
const contentType = response.headers.get("Content-Type") ?? "";
if (contentType.includes("application/json")) {
const body: unknown = await response
.clone()
.json()
.catch(() => undefined);
const payload = getApiCompatibilityPayload(body);
if (payload?.action.type === "reloadBrowser") {
toast.error(payload.message, {
id: "api-compatibility",
duration: Number.POSITIVE_INFINITY,
});
throw new Error(payload.message, { cause: payload });
}
}
}
return response;
};