koala73/worldmonitor · error

Cloud fallback blocked for

Error message

Cloud fallback blocked for ${target}

What it means

installRuntimeFetchPatch patches window.fetch in the Tauri desktop app so /api/* requests hit the local Node sidecar first and fall back to the remote cloud API when local fails. cloudFallback throws 'Cloud fallback blocked for <target>' when a fallback attempt is disallowed: either the target is local-only (isLocalOnlyApiTarget), the request is non-retryable (canRetryRequest, e.g. non-GET or streaming body), or the WORLDMONITOR_API_KEY secret is absent/invalid (secret-state check at src/services/runtime.ts:398-404).

Solutions

  1. Configure WORLDMONITOR_API_KEY in the desktop app's secret store so key-gated cloud fallback is allowed (check getSecretState('WORLDMONITOR_API_KEY').present/valid).
  2. If the target is intentionally local-only, don't expect cloud fallback — fix the local sidecar (is it running, correct port/token?) instead.
  3. Use a retryable request shape (GET without streaming body) or restructure the call so canRetryRequest passes.
  4. If the error appears only at cold start, the 2s secretsReady race may be too short — retry the request after startup completes or increase the grace period.
  5. Inspect with wm-debug-log=1 to see whether the block was due to allowCloudFallback or canRetryRequest and address the specific gate.

Example fix

// before
if (!allowCloudFallback || !canRetryRequest(input, init)) {
  throw new Error(`Cloud fallback blocked for ${target}`);
}
// after
if (!allowCloudFallback || !canRetryRequest(input, init)) {
  if (debug) console.log(`[fetch] cloud fallback blocked for ${target}: allow=${allowCloudFallback} retryable=${canRetryRequest(input, init)}`);
  throw new Error(`Cloud fallback blocked for ${target}`);
}
Defensive patterns

Strategy: validation

Validate before calling

const keyState = getSecretState('WORLDMONITOR_API_KEY');
const canFallBack = keyState.present && keyState.valid
  && !isLocalOnlyApiTarget(target)
  && canRetryRequest(input, init);
if (!canFallBack) {
  // don't rely on cloud fallback; surface local error or prompt for key setup
}

Type guard

function cloudFallbackAllowed(target: string, input: RequestInfo | URL, init?: RequestInit): boolean {
  if (isLocalOnlyApiTarget(target)) return false;
  if (!canRetryRequest(input, init)) return false;
  const s = getSecretState('WORLDMONITOR_API_KEY');
  return !!(s.present && s.valid);
}

Try / catch

try {
  return await patchedFetch('/api/some-endpoint');
} catch (err) {
  if (err.message.startsWith('Cloud fallback blocked')) {
    promptApiKeySetup(); // or show local-only error state
    return offlineResponse();
  }
  throw err;
}

Prevention

When it happens

Trigger: A /api/* request fails locally and code calls cloudFallback() when: (1) target is a local-only sidecar route (config/secrets endpoints) that must never proxy to cloud; (2) the request method/body makes it non-retryable; (3) the key-gated target requires WORLDMONITOR_API_KEY but the native secret state reports not present or not valid (or secretsReady timed out after 2s and the check threw).

Common situations: User running the desktop app without a configured WORLDMONITOR_API_KEY while requesting a key-gated endpoint whose local sidecar call failed; requesting a secrets/config route (by design local-only) that returned a non-ok status; sending POST/PUT with a body that can't be safely replayed to the cloud; secrets cache not yet initialized within the 2-second grace window at startup.

Related errors


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22). Data as JSON: /api/errors/44e3ed08f9259211. Report an issue: GitHub.

Appendix: source

Thrown at src/services/runtime.ts:409

    if (debug) console.log(`[fetch] intercept → ${target}`);
    let allowCloudFallback = !isLocalOnlyApiTarget(target) && canRetryRequest(input, init);

    if (allowCloudFallback && !isKeyFreeApiTarget(target)) {
      try {
        const { getSecretState, secretsReady } = await import('@/services/runtime-config');
        await Promise.race([secretsReady, new Promise<void>(r => setTimeout(r, 2000))]);
        const wmKeyState = getSecretState('WORLDMONITOR_API_KEY');
        if (!wmKeyState.present || !wmKeyState.valid) {
          allowCloudFallback = false;
        }
      } catch {
        allowCloudFallback = false;
      }
    }

    const cloudFallback = async () => {
      if (!allowCloudFallback || !canRetryRequest(input, init)) {
        throw new Error(`Cloud fallback blocked for ${target}`);
      }
      const cloudUrl = `${getRemoteApiBaseUrl()}${target}`;
      if (debug) console.log(`[fetch] cloud fallback → ${cloudUrl}`);
      return nativeFetch(input instanceof Request ? new Request(cloudUrl, input) : cloudUrl, init);
    };

    try {
      const t0 = performance.now();
      const response = await fetchLocalWithStartupRetry(target, input, init);
      if (debug) console.log(`[fetch] ${target} → ${response.status} (${Math.round(performance.now() - t0)}ms)`);

      if (!response.ok) {
        if (!allowCloudFallback) {
          if (debug) console.log(`[fetch] local-only endpoint ${target} returned ${response.status}; skipping cloud fallback`);
          return response;
        }
        if (debug) console.log(`[fetch] local ${response.status}, falling back to cloud`);
        return cloudFallback();

View on GitHub (pinned to e586b8b4b8)