koala73/worldmonitor · error · Error

Cloudflare Radar API error: ${resp.status}

Error message

Cloudflare Radar API error: ${resp.status}

What it means

fetchOutages() calls the Cloudflare Radar outage annotations endpoint with a Bearer token and a 15s timeout, and throws this error for any non-2xx HTTP status (401, 403, 429, 5xx, etc.). It surfaces only the numeric status, so diagnosing requires mapping the status code to its usual cause.

Solutions

  1. Check CLOUDFLARE_API_TOKEN is set and valid: curl the endpoint with `Authorization: Bearer $CLOUDFLARE_API_TOKEN` and read the response body/errors.
  2. Recreate the token with Cloudflare Radar read permission (Account > API Tokens) and reload it through loadEnvFile().
  3. For 429, back off and retry with exponential delay; stagger requests if seeding multiple Radar datasets.
  4. For 5xx, retry after a delay and include the response body in the error message for diagnosis; verify status on https://www.cloudflarestatus.com.

Example fix

// before
if (!resp.ok) throw new Error(`Cloudflare Radar API error: ${resp.status}`);
// after
if (!resp.ok) {
  const body = await resp.text().catch(() => '');
  if (resp.status === 429) { await sleep(backoffMs); return fetchOutages(); }
  throw new Error(`Cloudflare Radar API error: ${resp.status} ${body.slice(0, 500)}`);
}
Defensive patterns

Strategy: retry

Validate before calling

if (!process.env.CLOUDFLARE_API_TOKEN) {
  throw new Error('CLOUDFLARE_API_TOKEN is not set; cannot call Cloudflare Radar');
}
// smoke test before seeding:
const probe = await fetch(radarUrl, { headers: { Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 'User-Agent': CHROME_UA } });
if (!probe.ok) throw new Error(`Radar probe failed: ${probe.status} ${await probe.text()}`);

Try / catch

const fetchWithRetry = async (url, opts, attempts = 3) => {
  for (let i = 0; ; i++) {
    const resp = await fetch(url, opts);
    if (resp.ok) return resp;
    if ((resp.status === 429 || resp.status >= 500) && i < attempts - 1) {
      await new Promise(r => setTimeout(r, 2 ** i * 1000));
      continue;
    }
    throw new Error(`Cloudflare Radar API error: ${resp.status} ${(await resp.text().catch(() => '')).slice(0, 500)}`);
  }
};

Prevention

When it happens

Trigger: GET to the Radar outage annotations endpoint returns resp.ok === false: 401/403 for missing/invalid token or missing Radar permission, 429 for rate limiting, 5xx for Cloudflare-side errors, or network-level failures surfacing as non-OK statuses from a proxy.

Common situations: CLOUDFLARE_API_TOKEN unset, expired, or rotated without updating the environment; token created without the Radar API scope; bursty seeding loops hitting Radar's rate limits; transient Radar outages returning 5xx.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

Thrown at scripts/seed-internet-outages.mjs:146

  }
  return value;
}

async function fetchOutages() {
  const token = process.env.CLOUDFLARE_API_TOKEN;
  if (!token) {
    console.log('CLOUDFLARE_API_TOKEN not set — skipping');
    process.exit(0);
  }

  const resp = await fetch(`${CF_RADAR_URL}?dateRange=28d&limit=50`, {
    headers: {
      Authorization: `Bearer ${token}`,
      'User-Agent': CHROME_UA,
    },
    signal: AbortSignal.timeout(15_000),
  });
  if (!resp.ok) throw new Error(`Cloudflare Radar API error: ${resp.status}`);

  const result = requireRadarResult(await resp.json(), 'outage annotations');
  const annotations = requireRadarArray(result, 'annotations', 'outage annotations');

  const outages = [];
  for (const raw of annotations) {
    if (!raw.locations?.length) continue;
    const countryCode = raw.locations[0];
    if (!countryCode) continue;

    const coords = COUNTRY_COORDS[countryCode];
    if (!coords) continue;

    const countryName = raw.locationsDetails?.[0]?.name ?? countryCode;

    const categories = ['Cloudflare Radar'];
    if (raw.outage?.outageCause) categories.push(raw.outage.outageCause.replace(/_/g, ' '));
    if (raw.outage?.outageType) categories.push(raw.outage.outageType);

View on GitHub (pinned to 7d06c8633d)