windmill-labs/windmill · error

Offline replay: this page renders a recording and cannot cal

Error message

Offline replay: this page renders a recording and cannot call the API

What it means

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.

Source

Thrown at frontend/src/lib/components/recording/offlineReplay.svelte.ts:22

 * page may not even be served by the instance the recording came from.
 *
 * Two layers, because the player tree reaches far (LogViewer, DisplayResult,
 * FlowStatusViewer and everything they open):
 *  - components call {@link isReplaying} before fetching, or before rendering
 *    something whose `src`/`href` points at `/api`, so the UI degrades to the
 *    recorded data instead of showing a broken state;
 *  - {@link setOfflineReplay} additionally rejects every generated `*Service`
 *    call at the API client, so a path nobody thought to gate still issues no
 *    request. `EventSource` bypasses that client, but the only ones in the tree
 *    are JobLoader's (short-circuited by `getActiveReplay`) and the recorder's.
 */
import { OpenAPI } from '$lib/gen'
import { getActiveReplay } from './replay.svelte'

let offline = $state(false)

function rejectRequest(): never {
	throw new Error('Offline replay: this page renders a recording and cannot call the API')
}

/** True on the public page only. Use this for recorded *markup* whose rendering
 * would fetch subresources (`<img src>`, map tiles): the threat is content from an
 * arbitrary `?src=` origin on a page that promises to touch nothing, so the answer
 * is to not render it there. In-workspace the recording is one the user opened
 * themselves and the page makes no such promise, so it still renders. */
export function isOfflineReplay(): boolean {
	return offline
}

/** True whenever the UI shows recorded data rather than a live job: the whole
 * public page, or an in-workspace player while it replays a job stream. Use this
 * for anything about *staleness or side effects* — both cases want the recorded
 * value, not a fresh read (re-querying a ducklake table would answer for *now*
 * while the player replays a past run), and neither should let recorded data act. */
export function isReplaying(): boolean {
	return offline || getActiveReplay() != undefined

View on GitHub (pinned to e474e8803c)

Solutions

  1. Guard the API call with the offline check (isOfflineReplay()) and skip or use recorded data instead
  2. Ensure components rendered on the replay page read from the snapshot, not live endpoints
  3. If testing, close the replay/leave the public page before exercising live API paths

Example fix

// before
const settings = await getWorkspaceSettings(workspace)
// after
if (!isOfflineReplay()) {
  const settings = await getWorkspaceSettings(workspace)
} else {
  const settings = recordedSnapshot.settings
}
Defensive patterns

Strategy: validation

Validate before calling

import { isOfflineReplay } from '$lib/components/recording/offlineReplay.svelte'
if (isOfflineReplay()) return recordedFallback
data = await api.call(...)

Type guard

function canCallApi(): boolean { return !isOfflineReplay() }

Try / catch

try { data = await api.call() } catch (e) { if (/Offline replay/.test(String(e))) data = snapshotFallback; else throw e }

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/5cd2424e4e2b0eb0. Report an issue: GitHub.