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
getCountryProducts trims and uppercases req.iso2, then requires ^[A-Z]{2}$ and throws a 400 ValidationError otherwise. The comment marks a deliberate contract split: input-shape errors return 400 (legacy /api/supply-chain/v1/country-products behavior), while the PRO-gate deny path returns an empty 200 — so a 400 here always means a caller bug, not a permissions issue.
Solutions
- Send exactly 2 letters (ISO-3166-1 alpha-2), e.g. 'CN', after trim + toUpperCase
- Validate client-side with /^[A-Z]{2}$/ after uppercasing before the RPC
- Source codes from a fixed country list/dropdown instead of free text
Example fix
// before
getCountryProducts(ctx, { iso2: 'china' });
// after
getCountryProducts(ctx, { iso2: 'CN' }); 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'); Type guard
const isIso2 = (v: unknown): v is string => typeof v === 'string' && /^[A-Z]{2}$/.test(v.trim().toUpperCase()); Try / catch
catch (e) { if (e?.details?.[0]?.field === 'iso2') { normalize to alpha-2 and re-submit } else throw e; } Prevention
- Remember the contract: 400 = caller bug, empty 200 = not PRO — do not conflate them
- Map country names/ISO3 codes to alpha-2 client-side via a country list
- Skip the call when iso2 is unset rather than sending ''
When it happens
Trigger: Calling GetCountryProducts with iso2 missing, empty, 1 or 3+ characters, or containing non-letters ('usa', 'U', 'DEU', 'D1'). Lowercase 'de' passes because normalization uppercases first; only post-normalization failures throw.
Common situations: Passing ISO3 codes or country names from UI text inputs; empty string from an unset query param; client defaulting to undefined before the call; migration from an API that accepted 3-letter codes.
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…
- must be an ISO 3166-1 alpha-2 country code.
- A valid country code is required for a resilience score…
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/65c20850c93a1036.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/supply-chain/v1/get-country-products.ts:205
*/
function attachScale(product: CountryProduct, byCode?: Map<number, ExporterScale>): CountryProduct {
if (!byCode?.size) return product;
return {
...product,
topExporters: product.topExporters.map(exporter => {
const scale = byCode.get(exporter.partnerCode);
return scale && scale.year === product.year ? { ...exporter, scale } : exporter;
}),
};
}
export async function getCountryProducts(
ctx: ServerContext,
req: GetCountryProductsRequest,
): Promise<GetCountryProductsResponse> {
const iso2 = (req.iso2 ?? '').trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(iso2)) {
throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);
}
const hs4 = req.hs4?.trim();
if (hs4 && !HS4_CODES.includes(hs4)) {
throw new ValidationError([{ field: 'hs4', description: 'hs4 must be a supported four-digit heading' }]);
}
const isPro = await isCallerPremium(ctx.request);
const empty: GetCountryProductsResponse = { iso2, products: [], fetchedAt: '' };
if (!isPro) return empty;
const key = `comtrade:bilateral-hs4:${iso2}:v1`;
// Status-aware reads for the two country keys so a read error stays
// distinguishable from a miss; the canonical one decides cache_unavailable,
// the sibling one only decides how deep the origins go.
// The sibling detail and the world-exports snapshot are served only for a
// requested heading, so a whole-catalogue caller (the deep-dive panel) does
// not read them: the snapshot is a few hundred kilobytes (36 headings x ~140
// reporters), past what the 1.5 s single-GET deadline is sized for, and the
// large-value reader waits on the pipeline deadline instead.View on GitHub (pinned to 7d06c8633d)