{"record":{"id":"65c20850c93a1036","repo":"koala73/worldmonitor","slug":"iso2-must-be-a-2-letter-uppercase-iso-country-code","errorCode":null,"errorMessage":"iso2 must be a 2-letter uppercase ISO country code","messagePattern":"iso2 must be a 2-letter uppercase ISO country code","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/supply-chain/v1/get-country-products.ts","lineNumber":205,"sourceCode":" */\nfunction attachScale(product: CountryProduct, byCode?: Map<number, ExporterScale>): CountryProduct {\n  if (!byCode?.size) return product;\n  return {\n    ...product,\n    topExporters: product.topExporters.map(exporter => {\n      const scale = byCode.get(exporter.partnerCode);\n      return scale && scale.year === product.year ? { ...exporter, scale } : exporter;\n    }),\n  };\n}\n\nexport async function getCountryProducts(\n  ctx: ServerContext,\n  req: GetCountryProductsRequest,\n): Promise<GetCountryProductsResponse> {\n  const iso2 = (req.iso2 ?? '').trim().toUpperCase();\n  if (!/^[A-Z]{2}$/.test(iso2)) {\n    throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);\n  }\n  const hs4 = req.hs4?.trim();\n  if (hs4 && !HS4_CODES.includes(hs4)) {\n    throw new ValidationError([{ field: 'hs4', description: 'hs4 must be a supported four-digit heading' }]);\n  }\n  const isPro = await isCallerPremium(ctx.request);\n  const empty: GetCountryProductsResponse = { iso2, products: [], fetchedAt: '' };\n  if (!isPro) return empty;\n\n  const key = `comtrade:bilateral-hs4:${iso2}:v1`;\n  // Status-aware reads for the two country keys so a read error stays\n  // distinguishable from a miss; the canonical one decides cache_unavailable,\n  // the sibling one only decides how deep the origins go.\n  // The sibling detail and the world-exports snapshot are served only for a\n  // requested heading, so a whole-catalogue caller (the deep-dive panel) does\n  // not read them: the snapshot is a few hundred kilobytes (36 headings x ~140\n  // reporters), past what the 1.5 s single-GET deadline is sized for, and the\n  // large-value reader waits on the pipeline deadline instead.","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/supply-chain/v1/get-country-products.ts#L187-L223","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ngetCountryProducts(ctx, { iso2: 'china' });\n// after\ngetCountryProducts(ctx, { iso2: 'CN' });","handlingStrategy":"validation","validationCode":"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');","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 === 'iso2') { normalize to alpha-2 and re-submit } else throw e; }","preventionTips":["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 ''"],"tags":["validation","country-code","iso-3166","supply-chain","http-400"],"backgroundTag":"invalid-country-code","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}