{"record":{"id":"a12b43f5a6f23b83","repo":"koala73/worldmonitor","slug":"fromiso2-and-toiso2-must-be-valid-2-letter-iso-cou","errorCode":null,"errorMessage":"fromIso2 and toIso2 must be valid 2-letter ISO country codes","messagePattern":"fromIso2 and toIso2 must be valid 2-letter ISO country codes","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/route-intelligence.ts","lineNumber":50,"sourceCode":"}\n\ninterface ChokepointStatusResponse {\n  chokepoints?: ChokepointStatusEntry[];\n  upstreamUnavailable?: boolean;\n}\n\nconst VALID_CARGO_TYPES = new Set(['container', 'tanker', 'bulk', 'roro']);\n\nexport async function routeIntelligence(\n  ctx: ServerContext,\n  req: RouteIntelligenceRequest,\n): Promise<RouteIntelligenceResponse> {\n  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');\n\n  const fromIso2 = (req.fromIso2 ?? '').trim().toUpperCase();\n  const toIso2 = (req.toIso2 ?? '').trim().toUpperCase();\n  if (!/^[A-Z]{2}$/.test(fromIso2) || !/^[A-Z]{2}$/.test(toIso2)) {\n    throw new ValidationError([\n      { field: 'fromIso2', description: 'fromIso2 and toIso2 must be valid 2-letter ISO country codes' },\n    ]);\n  }\n\n  const cargoTypeRaw = (req.cargoType ?? '').trim().toLowerCase();\n  const cargoType: CargoType = (VALID_CARGO_TYPES.has(cargoTypeRaw) ? cargoTypeRaw : 'container') as CargoType;\n  const hs2 = (req.hs2 ?? '').trim().replace(/\\D/g, '') || '27';\n\n  const clusters = COUNTRY_PORT_CLUSTERS as unknown as Record<string, PortClusterEntry>;\n  const fromCluster = clusters[fromIso2];\n  const toCluster = clusters[toIso2];\n\n  const fromRoutes = new Set(fromCluster?.nearestRouteIds ?? []);\n  const toRoutes = new Set(toCluster?.nearestRouteIds ?? []);\n  const sharedRoutes = [...fromRoutes].filter(r => toRoutes.has(r));\n  const primaryRouteId = sharedRoutes[0] ?? fromCluster?.nearestRouteIds[0] ?? '';\n\n  const statusRaw = (await getCachedJson(CHOKEPOINT_STATUS_KEY).catch(() => null)) as ChokepointStatusResponse | null;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/route-intelligence.ts#L32-L68","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nrouteIntelligence(ctx, { fromIso2: 'usa', toIso2: 'JPN' });\n// after\nrouteIntelligence(ctx, { fromIso2: 'US', toIso2: 'JP' });","handlingStrategy":"validation","validationCode":"const norm = (v: string) => v.trim().toUpperCase();\nif (!/^[A-Z]{2}$/.test(norm(from)) || !/^[A-Z]{2}$/.test(norm(to))) throw new RangeError('fromIso2/toIso2 must be ISO-3166-1 alpha-2');","typeGuard":"const isIso2 = (v: unknown): v is string => typeof v === 'string' && /^[A-Z]{2}$/.test(v.trim().toUpperCase());","tryCatchPattern":"catch (e) { if (e?.details?.[0]?.field === 'fromIso2') { re-check both codes against ISO-3166-1 alpha-2 and re-submit } else throw e; }","preventionTips":["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"],"tags":["validation","country-code","iso-3166","shipping","http-400"],"backgroundTag":"invalid-country-code","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}