{"record":{"id":"44e3ed08f9259211","repo":"koala73/worldmonitor","slug":"cloud-fallback-blocked-for-target","errorCode":null,"errorMessage":"Cloud fallback blocked for ${target}","messagePattern":"Cloud fallback blocked for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/runtime.ts","lineNumber":409,"sourceCode":"    if (debug) console.log(`[fetch] intercept → ${target}`);\n    let allowCloudFallback = !isLocalOnlyApiTarget(target) && canRetryRequest(input, init);\n\n    if (allowCloudFallback && !isKeyFreeApiTarget(target)) {\n      try {\n        const { getSecretState, secretsReady } = await import('@/services/runtime-config');\n        await Promise.race([secretsReady, new Promise<void>(r => setTimeout(r, 2000))]);\n        const wmKeyState = getSecretState('WORLDMONITOR_API_KEY');\n        if (!wmKeyState.present || !wmKeyState.valid) {\n          allowCloudFallback = false;\n        }\n      } catch {\n        allowCloudFallback = false;\n      }\n    }\n\n    const cloudFallback = async () => {\n      if (!allowCloudFallback || !canRetryRequest(input, init)) {\n        throw new Error(`Cloud fallback blocked for ${target}`);\n      }\n      const cloudUrl = `${getRemoteApiBaseUrl()}${target}`;\n      if (debug) console.log(`[fetch] cloud fallback → ${cloudUrl}`);\n      return nativeFetch(input instanceof Request ? new Request(cloudUrl, input) : cloudUrl, init);\n    };\n\n    try {\n      const t0 = performance.now();\n      const response = await fetchLocalWithStartupRetry(target, input, init);\n      if (debug) console.log(`[fetch] ${target} → ${response.status} (${Math.round(performance.now() - t0)}ms)`);\n\n      if (!response.ok) {\n        if (!allowCloudFallback) {\n          if (debug) console.log(`[fetch] local-only endpoint ${target} returned ${response.status}; skipping cloud fallback`);\n          return response;\n        }\n        if (debug) console.log(`[fetch] local ${response.status}, falling back to cloud`);\n        return cloudFallback();","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/src/services/runtime.ts#L391-L427","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["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).","If the target is intentionally local-only, don't expect cloud fallback — fix the local sidecar (is it running, correct port/token?) instead.","Use a retryable request shape (GET without streaming body) or restructure the call so canRetryRequest passes.","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.","Inspect with wm-debug-log=1 to see whether the block was due to allowCloudFallback or canRetryRequest and address the specific gate."],"exampleFix":"// before\nif (!allowCloudFallback || !canRetryRequest(input, init)) {\n  throw new Error(`Cloud fallback blocked for ${target}`);\n}\n// after\nif (!allowCloudFallback || !canRetryRequest(input, init)) {\n  if (debug) console.log(`[fetch] cloud fallback blocked for ${target}: allow=${allowCloudFallback} retryable=${canRetryRequest(input, init)}`);\n  throw new Error(`Cloud fallback blocked for ${target}`);\n}","handlingStrategy":"validation","validationCode":"const keyState = getSecretState('WORLDMONITOR_API_KEY');\nconst canFallBack = keyState.present && keyState.valid\n  && !isLocalOnlyApiTarget(target)\n  && canRetryRequest(input, init);\nif (!canFallBack) {\n  // don't rely on cloud fallback; surface local error or prompt for key setup\n}","typeGuard":"function cloudFallbackAllowed(target: string, input: RequestInfo | URL, init?: RequestInit): boolean {\n  if (isLocalOnlyApiTarget(target)) return false;\n  if (!canRetryRequest(input, init)) return false;\n  const s = getSecretState('WORLDMONITOR_API_KEY');\n  return !!(s.present && s.valid);\n}","tryCatchPattern":"try {\n  return await patchedFetch('/api/some-endpoint');\n} catch (err) {\n  if (err.message.startsWith('Cloud fallback blocked')) {\n    promptApiKeySetup(); // or show local-only error state\n    return offlineResponse();\n  }\n  throw err;\n}","preventionTips":["Configure and validate WORLDMONITOR_API_KEY in the desktop secret store before using key-gated endpoints","Never assume cloud fallback exists for config/secret routes — they are local-only by design","Keep requests retryable (GET, non-streaming body) when cloud fallback matters","Extend the secretsReady grace window or await secrets before first API call at cold start","Enable wm-debug-log=1 when diagnosing which gate blocked the fallback"],"tags":["fetch","tauri","desktop","api-key","fallback"],"backgroundTag":"feature-not-enabled","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}