{"record":{"id":"5cd2424e4e2b0eb0","repo":"windmill-labs/windmill","slug":"offline-replay-this-page-renders-a-recording-and","errorCode":null,"errorMessage":"Offline replay: this page renders a recording and cannot call the API","messagePattern":"Offline replay: this page renders a recording and cannot call the API","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/recording/offlineReplay.svelte.ts","lineNumber":22,"sourceCode":" * page may not even be served by the instance the recording came from.\n *\n * Two layers, because the player tree reaches far (LogViewer, DisplayResult,\n * FlowStatusViewer and everything they open):\n *  - components call {@link isReplaying} before fetching, or before rendering\n *    something whose `src`/`href` points at `/api`, so the UI degrades to the\n *    recorded data instead of showing a broken state;\n *  - {@link setOfflineReplay} additionally rejects every generated `*Service`\n *    call at the API client, so a path nobody thought to gate still issues no\n *    request. `EventSource` bypasses that client, but the only ones in the tree\n *    are JobLoader's (short-circuited by `getActiveReplay`) and the recorder's.\n */\nimport { OpenAPI } from '$lib/gen'\nimport { getActiveReplay } from './replay.svelte'\n\nlet offline = $state(false)\n\nfunction rejectRequest(): never {\n\tthrow new Error('Offline replay: this page renders a recording and cannot call the API')\n}\n\n/** True on the public page only. Use this for recorded *markup* whose rendering\n * would fetch subresources (`<img src>`, map tiles): the threat is content from an\n * arbitrary `?src=` origin on a page that promises to touch nothing, so the answer\n * is to not render it there. In-workspace the recording is one the user opened\n * themselves and the page makes no such promise, so it still renders. */\nexport function isOfflineReplay(): boolean {\n\treturn offline\n}\n\n/** True whenever the UI shows recorded data rather than a live job: the whole\n * public page, or an in-workspace player while it replays a job stream. Use this\n * for anything about *staleness or side effects* — both cases want the recorded\n * value, not a fresh read (re-querying a ducklake table would answer for *now*\n * while the player replays a past run), and neither should let recorded data act. */\nexport function isReplaying(): boolean {\n\treturn offline || getActiveReplay() != undefined","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/recording/offlineReplay.svelte.ts#L4-L40","documentation":"On the public offline replay page, every API call is intentionally blocked: the page renders a recorded snapshot and promises to touch nothing outside it. rejectRequest is the common sink that OpenAPI requests go through when offline mode is active, so any attempted network call throws instead of leaking requests from an untrusted page.","triggerScenarios":"Any code path that calls the Windmill API (via OpenAPI client) while a recording is being rendered in offline replay mode, i.e. while getActiveReplay() is set and offline is true.","commonSituations":"A recording captured markup or a component that lazily fetches data on mount; opening a shared recording link that triggers background refreshes (drafts, job status, workspace settings).","solutions":["Guard the API call with the offline check (isOfflineReplay()) and skip or use recorded data instead","Ensure components rendered on the replay page read from the snapshot, not live endpoints","If testing, close the replay/leave the public page before exercising live API paths"],"exampleFix":"// before\nconst settings = await getWorkspaceSettings(workspace)\n// after\nif (!isOfflineReplay()) {\n  const settings = await getWorkspaceSettings(workspace)\n} else {\n  const settings = recordedSnapshot.settings\n}","handlingStrategy":"validation","validationCode":"import { isOfflineReplay } from '$lib/components/recording/offlineReplay.svelte'\nif (isOfflineReplay()) return recordedFallback\ndata = await api.call(...)","typeGuard":"function canCallApi(): boolean { return !isOfflineReplay() }","tryCatchPattern":"try { data = await api.call() } catch (e) { if (/Offline replay/.test(String(e))) data = snapshotFallback; else throw e }","preventionTips":["Gate every fetch-heavy component with the offline flag when rendered on replay pages","Prefer snapshot data over live endpoints inside recording components","Test recorded pages headlessly to catch stray API calls"],"tags":["recording","offline","api-blocked","security"],"backgroundTag":"offline-replay-api-blocked","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}