koala73/worldmonitor · warning · ValidationError
fromIso2 and toIso2 must be valid 2-letter ISO country codes
Error message
fromIso2 and toIso2 must be valid 2-letter ISO country codes
What it means
routeIntelligence trims and uppercases fromIso2/toIso2, then requires both to match ^[A-Z]{2}$ and throws a 400 on the fromIso2 field (one message covers both). It fires after the PRO gate and before the COUNTRY_PORT_CLUSTERS lookup, so an invalid pair never reaches routing logic.
Source
Thrown at server/worldmonitor/shipping/v2/route-intelligence.ts:50
}
interface ChokepointStatusResponse {
chokepoints?: ChokepointStatusEntry[];
upstreamUnavailable?: boolean;
}
const VALID_CARGO_TYPES = new Set(['container', 'tanker', 'bulk', 'roro']);
export async function routeIntelligence(
ctx: ServerContext,
req: RouteIntelligenceRequest,
): Promise<RouteIntelligenceResponse> {
await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');
const fromIso2 = (req.fromIso2 ?? '').trim().toUpperCase();
const toIso2 = (req.toIso2 ?? '').trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(fromIso2) || !/^[A-Z]{2}$/.test(toIso2)) {
throw new ValidationError([
{ field: 'fromIso2', description: 'fromIso2 and toIso2 must be valid 2-letter ISO country codes' },
]);
}
const cargoTypeRaw = (req.cargoType ?? '').trim().toLowerCase();
const cargoType: CargoType = (VALID_CARGO_TYPES.has(cargoTypeRaw) ? cargoTypeRaw : 'container') as CargoType;
const hs2 = (req.hs2 ?? '').trim().replace(/\D/g, '') || '27';
const clusters = COUNTRY_PORT_CLUSTERS as unknown as Record<string, PortClusterEntry>;
const fromCluster = clusters[fromIso2];
const toCluster = clusters[toIso2];
const fromRoutes = new Set(fromCluster?.nearestRouteIds ?? []);
const toRoutes = new Set(toCluster?.nearestRouteIds ?? []);
const sharedRoutes = [...fromRoutes].filter(r => toRoutes.has(r));
const primaryRouteId = sharedRoutes[0] ?? fromCluster?.nearestRouteIds[0] ?? '';
const statusRaw = (await getCachedJson(CHOKEPOINT_STATUS_KEY).catch(() => null)) as ChokepointStatusResponse | null;View on GitHub (pinned to eeab0a219f)
Solutions
- Send 2-letter ISO-3166-1 alpha-2 codes for both fromIso2 and toIso2
- Constrain client input to a country selector keyed by alpha-2 codes
- Normalize client-side (trim + toUpperCase) and validate against /^[A-Z]{2}$/ before sending
Example fix
// before
routeIntelligence(ctx, { fromIso2: 'usa', toIso2: 'JPN' });
// after
routeIntelligence(ctx, { fromIso2: 'US', toIso2: 'JP' }); Defensive patterns
Strategy: validation
Validate before calling
const norm = (v: string) => v.trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(norm(from)) || !/^[A-Z]{2}$/.test(norm(to))) throw new RangeError('fromIso2/toIso2 must be ISO-3166-1 alpha-2'); 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 === 'fromIso2') { re-check both codes against ISO-3166-1 alpha-2 and re-submit } else throw e; } Prevention
- Use alpha-2 codes from a fixed country list, never free text
- Normalize (trim, uppercase) before sending — the server does too, but bad lengths still fail
- Do not send ISO3 or numeric country codes
When it happens
Trigger: Calling RouteIntelligence with fromIso2 or toIso2 missing, empty, 1 or 3+ characters, or containing digits/punctuation ('usa', 'U', 'US1', 'U.S.'). Lowercase input like 'us' is fine because the handler uppercases before testing — only post-normalization failures throw.
Common situations: Free-text country input not constrained to ISO-3166-1 alpha-2; clients sending ISO3 ('USA') or numeric M49 codes; passing a full country name from a form field.
Related errors
- iso2 must be a 2-letter uppercase ISO country code
- iso2 must be a 2-letter uppercase ISO country code
- Unknown chokepoint ID: ${invalidCp}
- country must be an ISO 3166-1 alpha-2 country code, e.g. "UA
- must be an ISO 3166-1 alpha-2 country code.
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/a12b43f5a6f23b83.
Report an issue: GitHub.