koala73/worldmonitor · warning · ValidationError
iso2 must be a 2-letter uppercase ISO country code
Error message
iso2 must be a 2-letter uppercase ISO country code
What it means
getMultiSectorCostShock applies the same contract as its sibling endpoints: it trims/uppercases req.iso2 and throws a 400 ValidationError when the value does not match ^[A-Z]{2}$, reserving the empty-200 shape for the PRO-gate deny path. The iso2 check is the first of three validations — chokepointId-required, then chokepointId-registry, then the PRO gate.
Source
Thrown at server/worldmonitor/supply-chain/v1/get-multi-sector-cost-shock.ts:80
unavailableReason,
};
}
export async function getMultiSectorCostShock(
ctx: ServerContext,
req: GetMultiSectorCostShockRequest,
): Promise<GetMultiSectorCostShockResponse> {
const iso2 = (req.iso2 ?? '').trim().toUpperCase();
const chokepointId = (req.chokepointId ?? '').trim().toLowerCase();
const closureDays = clampClosureDays(req.closureDays ?? 30);
// Input-shape errors return 400 — restoring the legacy /api/supply-chain/v1/
// multi-sector-cost-shock contract. Empty-payload-200 is reserved for the
// PRO-gate deny path (intentional contract shift), not for caller bugs
// (malformed or missing fields). Distinguishing the two matters for external
// API consumers, tests, and silent-failure detection in logs.
if (!/^[A-Z]{2}$/.test(iso2)) {
throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);
}
if (!chokepointId) {
throw new ValidationError([{ field: 'chokepointId', description: 'chokepointId is required' }]);
}
if (!CHOKEPOINT_REGISTRY.some(c => c.id === chokepointId)) {
throw new ValidationError([{ field: 'chokepointId', description: `Unknown chokepointId: ${chokepointId}` }]);
}
const isPro = await isCallerPremium(ctx.request);
if (!isPro) return emptyResponse(iso2, chokepointId, closureDays);
// Seeder writes the products payload via raw key (no env-prefix) — read raw.
const productsKey = `comtrade:bilateral-hs4:${iso2}:v1`;
const [productsCache, statusCache] = await Promise.all([
getCachedJson(productsKey, true).catch(() => null) as Promise<CountryProductsCache | null>,
getCachedJson(CHOKEPOINT_STATUS_KEY).catch(() => null) as Promise<{ chokepoints?: ChokepointInfo[] } | null>,
]);
View on GitHub (pinned to eeab0a219f)
Solutions
- Send a 2-letter ISO-3166-1 alpha-2 code after trim + toUpperCase
- Validate both iso2 (^[A-Z]{2}$) and chokepointId (present, in CHOKEPOINT_REGISTRY) client-side before the call
- Pull codes from the same chokepoint/country registries the service uses
Example fix
// before
getMultiSectorCostShock(ctx, { iso2: 'germany', chokepointId: 'panama-canal' });
// after
getMultiSectorCostShock(ctx, { iso2: 'DE', chokepointId: 'panama-canal' }); Defensive patterns
Strategy: validation
Validate before calling
const iso2 = value.trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(iso2)) throw new RangeError('iso2 must be a 2-letter ISO-3166-1 alpha-2 code');
if (!chokepointId) throw new RangeError('chokepointId is required'); Type guard
const isIso2 = (v: unknown): v is string => typeof v === 'string' && /^[A-Z]{2}$/.test(v.trim().toUpperCase()); Try / catch
catch (e) { const f = e?.details?.[0]?.field; if (f === 'iso2') { fix the country code and re-submit } else if (f === 'chokepointId') { supply a registry chokepointId } else throw e; } Prevention
- Validate iso2 and chokepointId together before the call — both 400s are caller bugs, empty 200 means PRO-gated
- Source country codes from a fixed alpha-2 list, not free text
- Reuse the same normalization (trim + uppercase) the server applies
When it happens
Trigger: Calling GetMultiSectorCostShock with iso2 missing, empty, wrong length ('U', 'DEU'), or containing non-letters ('D1', 'germany'). chokepointId defaults like closureDays do not save you — a bad iso2 throws before any other field is consulted.
Common situations: Reusing a country name or ISO3 code from another dataset; unset query params rendering as ''; shared request-builder code that skips the iso2 field; tests with placeholder values like 'XX1'.
Related errors
- iso2 must be a 2-letter uppercase ISO country code
- fromIso2 and toIso2 must be valid 2-letter ISO country codes
- country must be an ISO 3166-1 alpha-2 country code, e.g. "UA
- must be an ISO 3166-1 alpha-2 country code.
- callbackUrl is required
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/b7b463f5d510b517.
Report an issue: GitHub.