koala73/worldmonitor · error · Error

CF Radar DDoS API error: protocol=${protocolResp.status} vec

Error message

CF Radar DDoS API error: protocol=${protocolResp.status} vector=${vectorResp.status}

What it means

fetchDdosData fetches the Cloudflare Radar layer3 attacks summary endpoints for protocol and vector in parallel (plus the optional target-locations fetch) and throws this combined error when either summary response is non-2xx. Unlike [145], this one is treated as fatal: without protocol/vector summaries the DDoS record cannot be written at all. The message embeds both status codes so you can tell at a glance which of the two upstream calls failed.

Solutions

  1. Read protocol=NNN vector=NNN in the message: the non-zero/non-200 side is the failing endpoint; curl it with the same headers to reproduce.
  2. 401/403: regenerate the Cloudflare token with Radar read permissions and update the seed env; 429: add backoff/spacing between seed runs; 5xx: retry later.
  3. Add one retry with short backoff around the two fetches to ride out transient 5xx/429.
  4. Confirm the responses are also valid JSON afterwards — a 200 with an HTML error page would instead fail later in requireRadarResult.

Example fix

null
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight both endpoints cheaply:
for (const path of ['radar/attacks/layer3/summary/protocol', 'radar/attacks/layer3/summary/vector']) {
  const r = await fetch(`${CF_RADAR_BASE}/${path}?dateRange=7d`, { headers });
  if (!r.ok) throw new Error(`precheck ${path}: ${r.status}`);
}

Try / catch

try {
  const ddos = await fetchDdosData(headers);
} catch (err) {
  const m = err.message.match(/protocol=(\d+) vector=(\d+)/);
  if (m && (m[1] === '429' || +m[1] >= 500 || m[2] === '429' || +m[2] >= 500)) {
    await sleep(backoff); // then retry once before giving up
  } else throw err;
}

Prevention

When it happens

Trigger: Either `GET /radar/attacks/layer3/summary/protocol?dateRange=7d` or `GET /radar/attacks/layer3/summary/vector?dateRange=7d` returns non-2xx (401/403 bad token, 429 rate limit, 5xx outage) after the 15s timeout window, triggering `if (!protocolResp.ok || !vectorResp.ok) throw`.

Common situations: Expired Cloudflare API token in the seed environment; Radar API partial outage affecting one summary route; running seeds frequently enough to trip Radar rate limits; token scoped to the wrong account/plan.

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/c523df9c0d1b2fa4. Report an issue: GitHub.

Appendix: source

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

      return {
        items: requireRadarArray(result, 'top_0', 'DDoS target locations')
          .filter((item) => item && typeof item === 'object' && !Array.isArray(item)),
        degraded: false,
      };
    } catch (err) {
      console.warn(`  CF Radar DDoS target locations unavailable (optional slice): ${err?.message || err}`);
      return { items: [], degraded: true };
    }
  };

  const [protocolResp, vectorResp, targetSlice] = await Promise.all([
    fetch(`${CF_RADAR_BASE}/radar/attacks/layer3/summary/protocol?dateRange=7d`, { headers, signal: AbortSignal.timeout(15_000) }),
    fetch(`${CF_RADAR_BASE}/radar/attacks/layer3/summary/vector?dateRange=7d`, { headers, signal: AbortSignal.timeout(15_000) }),
    fetchOptionalTargetLocations(),
  ]);

  if (!protocolResp.ok || !vectorResp.ok) {
    throw new Error(`CF Radar DDoS API error: protocol=${protocolResp.status} vector=${vectorResp.status}`);
  }

  const [protocolResult, vectorResult] = await Promise.all([
    protocolResp.json().then((data) => requireRadarResult(data, 'DDoS protocol')),
    vectorResp.json().then((data) => requireRadarResult(data, 'DDoS vector')),
  ]);

  function toEntries(summary) {
    return Object.entries(summary).map(([label, pct]) => ({ label, percentage: parseFloat(pct) || 0 }))
      .sort((a, b) => b.percentage - a.percentage);
  }

  const topTargetLocations = targetSlice.items.map((item) => {
    const code = item.clientCountryAlpha2 || '';
    const coords = COUNTRY_COORDS[code] || null;
    return {
      countryCode: code,
      countryName: item.clientCountryName || code,

View on GitHub (pinned to 7d06c8633d)