koala73/worldmonitor · warning · EmbedMapFrameUnavailableError

Embed map frame temporarily unavailable

Error message

Embed map frame temporarily unavailable

What it means

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.

Solutions

  1. Wait for the retry-after delay carried by EmbedMapFrameUnavailableError, then retry the frame request.
  2. Implement exponential backoff with jitter instead of immediate retries to avoid deepening the rate limit.
  3. Cache the last successfully fetched frame and render it stale while retrying in the background.
  4. Check the embed endpoint's server logs/metrics if 5xx persists (deployment or upstream issue).

Example fix

// before
const frame = await fetchEmbedMapFrame(params);
// after
try {
  const frame = await fetchEmbedMapFrame(params);
} catch (e) {
  if (e instanceof EmbedMapFrameUnavailableError) {
    setTimeout(() => fetchEmbedMapFrame(params).then(render), e.retryAfterMs);
    render(staleFrame);
    return;
  }
  throw e;
}
Defensive patterns

Strategy: retry

Type guard

const isEmbedUnavailable = (e: unknown): e is EmbedMapFrameUnavailableError => e instanceof EmbedMapFrameUnavailableError;

Try / catch

try {
  const frame = await fetchEmbedMapFrame(params);
} catch (e) {
  if (isEmbedUnavailable(e)) {
    setTimeout(() => void fetchEmbedMapFrame(params).then(render).catch(console.warn), e.retryAfterMs);
    render(cachedFrame ?? placeholder);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: The GET request for an embed map frame returns status 429 (rate limited) or >= 500 (server error) from the edge/backend serving embed frames.

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

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/f7d2b9421735c869. Report an issue: GitHub.

Appendix: source

Thrown at src/embed/embed-fetch.ts:184

  const headers: Record<string, string> = { Accept: 'application/json' };

  let target: string;
  if (grant) {
    headers['X-WorldMonitor-Grant'] = grant.token;
    const url = new URL(EMBED_MAP_FRAME_PATH, origin);
    url.searchParams.set('layers', canonicalizeEmbedLayers(layerIds).join(','));
    target = url.toString();
  } else {
    target = `${origin}${EMBED_MAP_FRAME_PATH}?${publicFrameSearch(layerIds)}`;
  }

  const resp = await globalThis.fetch(target, {
    method: 'GET',
    headers,
    credentials: 'omit',
  });
  if (resp.status === 429 || resp.status >= 500) {
    throw new EmbedMapFrameUnavailableError(frameRetryAfterMs(resp));
  }
  if (!resp.ok) throw new Error(`Embed map frame request failed: ${resp.status}`);
  return await resp.json() as EmbedMapFrameResponse;
}

/**
 * The cacheable query for the free layers among `layerIds`, falling back to
 * the plain uncacheable query when none qualify.
 *
 * Paid layers are dropped from the keyless URL rather than carried into it:
 * a keyless caller cannot receive them either way, and naming them would
 * multiply the shared key space past its seven free subsets. The frame marks
 * them un-ready from their absence in the response, which is the same outcome
 * an explicit `not-entitled` would produce.
 */
function publicFrameSearch(layerIds: readonly EmbedLayerId[]): string {
  const free = getEmbedPanelFreeTier('map');
  const eligible = free

View on GitHub (pinned to 7d06c8633d)