{"record":{"id":"f7d2b9421735c869","repo":"koala73/worldmonitor","slug":"embed-map-frame-temporarily-unavailable","errorCode":null,"errorMessage":"Embed map frame temporarily unavailable","messagePattern":"Embed map frame temporarily unavailable","errorType":"exception","errorClass":"EmbedMapFrameUnavailableError","httpStatus":429,"severity":"warning","filePath":"src/embed/embed-fetch.ts","lineNumber":184,"sourceCode":"  const headers: Record<string, string> = { Accept: 'application/json' };\n\n  let target: string;\n  if (grant) {\n    headers['X-WorldMonitor-Grant'] = grant.token;\n    const url = new URL(EMBED_MAP_FRAME_PATH, origin);\n    url.searchParams.set('layers', canonicalizeEmbedLayers(layerIds).join(','));\n    target = url.toString();\n  } else {\n    target = `${origin}${EMBED_MAP_FRAME_PATH}?${publicFrameSearch(layerIds)}`;\n  }\n\n  const resp = await globalThis.fetch(target, {\n    method: 'GET',\n    headers,\n    credentials: 'omit',\n  });\n  if (resp.status === 429 || resp.status >= 500) {\n    throw new EmbedMapFrameUnavailableError(frameRetryAfterMs(resp));\n  }\n  if (!resp.ok) throw new Error(`Embed map frame request failed: ${resp.status}`);\n  return await resp.json() as EmbedMapFrameResponse;\n}\n\n/**\n * The cacheable query for the free layers among `layerIds`, falling back to\n * the plain uncacheable query when none qualify.\n *\n * Paid layers are dropped from the keyless URL rather than carried into it:\n * a keyless caller cannot receive them either way, and naming them would\n * multiply the shared key space past its seven free subsets. The frame marks\n * them un-ready from their absence in the response, which is the same outcome\n * an explicit `not-entitled` would produce.\n */\nfunction publicFrameSearch(layerIds: readonly EmbedLayerId[]): string {\n  const free = getEmbedPanelFreeTier('map');\n  const eligible = free","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/embed/embed-fetch.ts#L166-L202","documentation":"fetchEmbedMapFrame treats HTTP 429 and any 5xx from the embed map frame endpoint as temporary unavailability and throws EmbedMapFrameUnavailableError, carrying a retry-after delay derived from the response. Unlike a generic failure, this error signals the caller may retry after the suggested delay.","triggerScenarios":"The GET request for an embed map frame returns status 429 (rate limited) or >= 500 (server error) from the edge/backend serving embed frames.","commonSituations":"Rate limiting after many embed requests from one origin/IP; backend or Railway worker down or redeploying; upstream data dependency of the frame endpoint failing; load spikes causing 502/503 from the proxy.","solutions":["Wait for the retry-after delay carried by EmbedMapFrameUnavailableError, then retry the frame request.","Implement exponential backoff with jitter instead of immediate retries to avoid deepening the rate limit.","Cache the last successfully fetched frame and render it stale while retrying in the background.","Check the embed endpoint's server logs/metrics if 5xx persists (deployment or upstream issue)."],"exampleFix":"// before\nconst frame = await fetchEmbedMapFrame(params);\n// after\ntry {\n  const frame = await fetchEmbedMapFrame(params);\n} catch (e) {\n  if (e instanceof EmbedMapFrameUnavailableError) {\n    setTimeout(() => fetchEmbedMapFrame(params).then(render), e.retryAfterMs);\n    render(staleFrame);\n    return;\n  }\n  throw e;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"const isEmbedUnavailable = (e: unknown): e is EmbedMapFrameUnavailableError => e instanceof EmbedMapFrameUnavailableError;","tryCatchPattern":"try {\n  const frame = await fetchEmbedMapFrame(params);\n} catch (e) {\n  if (isEmbedUnavailable(e)) {\n    setTimeout(() => void fetchEmbedMapFrame(params).then(render).catch(console.warn), e.retryAfterMs);\n    render(cachedFrame ?? placeholder);\n    return;\n  }\n  throw e;\n}","preventionTips":["Honor Retry-After and add exponential backoff with jitter.","Cache the last good frame for stale-while-revalidate rendering.","Space out embed requests to stay under rate limits."],"tags":["http","rate-limit","retry","embed"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}