koala73/worldmonitor · error
get-vessel-snapshot HTTP
Error message
get-vessel-snapshot HTTP ${res.status}${detail ? ` — ${detail}` : ''} What it means
Plain Error thrown in get_vessel_snapshot (api/mcp/registry/rpc-tools.ts:1828): the maritime snapshot endpoint returned non-ok within the 8-second AbortSignal budget. Billing denials are checked first (throwIfBillingDenial with label 'get-vessel-snapshot'), so reaching this throw means an ordinary HTTP failure. The message embeds the status plus up to 200 chars of the response body as detail, giving enough context to classify auth vs upstream vs parameter failures.
Solutions
- Parse the status and the — detail suffix: 401/403 → audit auth signing; 4xx with a message → fix the country/bbox params; 5xx → retry
- Verify the country param is ISO 3166-1 alpha-2 and supported by the maritime snapshot endpoint
- Reproduce with curl GET on the same path+query with the same auth headers to isolate signature vs upstream
- Retry with backoff on 5xx; the fetch budget is 8s, so persistent timeouts indicate provider degradation
Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the country param before calling
if (!/^[A-Za-z]{2}$/.test(country ?? '')) throw new Error('country must be ISO 3166-1 alpha-2'); Type guard
function isVesselHttpError(e) {
return e instanceof Error && /^get-vessel-snapshot HTTP \d+/.test(e.message);
}
function vesselStatusOf(e) { return Number(e.message.match(/HTTP (\d+)/)?.[1] ?? 0); } Try / catch
try {
const snap = await client.callTool('get_vessel_snapshot', { country: 'AE' });
} catch (e) {
if (isVesselHttpError(e)) {
const s = vesselStatusOf(e);
if (s >= 500) return retryWithBackoff(call, 3);
if (s === 401 || s === 403) throw new Error('Vessel snapshot auth failed — check signing of the GET path+query', { cause: e });
throw e;
}
throw e;
} Prevention
- Include the full query string when signing GET requests for this path
- The message carries up to 200 chars of body detail — log it verbatim for fast classification
- The fetch aborts at 8s; schedule retries around provider recovery rather than hammering
When it happens
Trigger: Calling get_vessel_snapshot when: the HMAC auth header is wrong/expired (401/403), entitlement lapsed without the billing marker, an unsupported/invalid country or bbox yields 4xx, or the maritime upstream 5xxes/times out behind the gateway.
Common situations: Signature drift for GET requests (query-string canonicalization in buildAuthHeaders). Invalid ISO country code for the maritime zone lookup. Maritime data provider outage. Preview/local deployments missing the signing secret.
Related errors
- get-country-intel-brief HTTP
- body-too-large
- Could not resolve to a country.
- country must be an ISO 3166-1 alpha-2 country code, e.g…
- get-china-decision-signals returned an invalid canonical…
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/219df6c159e9e7e6.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp/registry/rpc-tools.ts:2635
return { error: `Could not resolve ${JSON.stringify(echoCountryInput(params.country_code))} to a country. ${COUNTRY_ARG_HINT}` };
}
const bbox = COUNTRY_BBOXES[code];
if (!bbox) return { error: `No maritime coverage for ${code}: that country has no bounding box in the dataset.` };
const [sw_lat, sw_lon, ne_lat, ne_lon] = bbox;
// Deliberately NO bbox on the inner fetch: the handler rejects any bbox
// dimension >10° (BboxValidationError → HTTP 400), and 67 of the 167
// COUNTRY_BBOXES exceed that (US, JP, AU, BR, …) — WORLDMONITOR-T8.
// The relay's density/disruption sets are global regardless of bbox
// (bbox only scopes tanker/candidate reports, which this tool never
// requests), so we take the cached global snapshot and filter to the
// country bbox here using each item's coordinates.
const url = `${base}/api/maritime/v1/get-vessel-snapshot`;
const auth = await buildAuthHeaders(context, 'GET', url, null);
// Wire shape is the generated sebuf JSON — camelCase field names with
// nested `location` (the previous snake_case reads matched nothing, so
// density_zones was permanently empty).
type VesselLoc = { latitude?: number; longitude?: number };
type VesselResp = {
snapshot?: {
snapshotAt?: number;
densityZones?: { name?: string; location?: VesselLoc; intensity?: number; shipsPerDay?: number; deltaPct?: number; note?: string }[];
disruptions?: { name?: string; type?: string; severity?: string; location?: VesselLoc; darkShips?: number; vesselCount?: number; region?: string; description?: string }[];
};
};
const res = await fetchMcpDownstream(url, {
headers: { ...auth, 'User-Agent': 'worldmonitor-mcp-edge/1.0' },
signal: AbortSignal.timeout(8_000),
}, execution);
if (!res.ok) {
throwIfBillingDenial(res, 'get-vessel-snapshot');
const detail = (await res.text().catch(() => '')).slice(0, 200);
throw new Error(`get-vessel-snapshot HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
}
const data = await res.json() as VesselResp;View on GitHub (pinned to e586b8b4b8)