koala73/worldmonitor · error

HTTP ${response.status}

Error message

HTTP ${response.status}

What it means

fetchParts fetches multiple upstream URLs in parallel and throws a generic Error(`HTTP ${status}`) when any part returns a non-ok response (after billing denials are handled separately). The message is intentionally terse; the actual status code is embedded in the message and the specific upstream body is not surfaced.

Solutions

  1. Read the thrown message's status code and check the corresponding upstream endpoint's health/logs
  2. Fix the URL/path or credentials that produce the non-2xx status
  3. Add retry-with-backoff for transient 5xx statuses
  4. Consider enriching the error with the URL/status context at the call site to ease diagnosis

Example fix

// before
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// after
if (!response.ok) throw new Error(`HTTP ${response.status} from ${url}: ${await response.text().catch(() => '')}`);
Defensive patterns

Strategy: retry

Validate before calling

// pre-flight reachability
const probe = await fetch(url, { method: 'HEAD' }); if (!probe.ok) throw new Error(`upstream ${url} unhealthy: ${probe.status}`);

Type guard

null

Try / catch

try { return await fetchParts(urls, op); } catch (e) { if (/^HTTP 5\d\d$/.test(e.message)) return retryWithBackoff(() => fetchParts(urls, op), 3); throw e; }

Prevention

When it happens

Trigger: Any RPC registry tool that fans out to multiple upstream endpoints via fetchParts where one endpoint returns 404/500/502/503, or an auth-less URL (buildAuthHeaders returning null yields null parts, but a bad-token call yields 401). Timeouts are separately handled by AbortSignal.timeout(8000).

Common situations: Upstream endpoint path changed or removed (404); upstream service degraded (5xx); expired credentials returning 401; calling a URL built from stale config.

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

Appendix: source

Thrown at api/mcp/registry/rpc-tools.ts:2480

      }
      const bbox = COUNTRY_BBOXES[code];
      if (!bbox) return { error: `No airspace coverage for ${code}: that country has no bounding box in the dataset.` };
      const box = countryBox(code);
      const queryBoxes = box ? splitCountryBox(box) : [];
      if (!queryBoxes.length) return { error: `No airspace coverage for ${code}: its full-longitude extent cannot scope a country flight query.` };
      const [sw_lat, sw_lon, ne_lat, ne_lon] = bbox;
      const type = String(params.type ?? 'all');
      const UA = 'worldmonitor-mcp-edge/1.0';
      const queries = queryBoxes.map(bounds =>
        `sw_lat=${bounds.south}&sw_lon=${bounds.west}&ne_lat=${bounds.north}&ne_lon=${bounds.east}`);

      async function fetchParts<T>(urls: string[], operation: string): Promise<(T | null)[]> {
        const parts = await Promise.allSettled(urls.map(async url => {
          const auth = await buildAuthHeaders(context, 'GET', url, null);
          if (!auth) return null;
          const response = await fetch(url, { headers: { ...auth, 'User-Agent': UA }, signal: AbortSignal.timeout(8_000) });
          throwIfBillingDenial(response, operation);
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          return response.json() as Promise<T>;
        }));
        // A failure in one half must not hide a billing denial in the other.
        for (const part of parts) {
          if (part.status === 'rejected' && part.reason instanceof BillingDenialError) throw part.reason;
        }
        return parts.map(part => {
          if (part.status === 'rejected') throw part.reason;
          return part.value;
        });
      }

      const [civResult, milResult] = await Promise.allSettled([
        type === 'military' ? Promise.resolve(null) : fetchParts<TrackAircraftResponse>(
          queries.map(query => `${base}/api/aviation/v1/track-aircraft?${query}`), 'get-airspace-civilian'),
        type === 'civilian' ? Promise.resolve(null) : fetchParts<ListMilitaryFlightsResponse>(
          queries.map(query => `${base}/api/military/v1/list-military-flights?${query}&page_size=100`), 'get-airspace-military'),
      ]);

View on GitHub (pinned to 7d06c8633d)