{"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":30,"sourceCode":"interface BilateralHs4Payload {\n  iso2: string;\n  products?: CountryProduct[];\n  fetchedAt?: string;\n}\n\nexport async function getCountryProducts(\n  ctx: ServerContext,\n  req: GetCountryProductsRequest,\n): Promise<GetCountryProductsResponse> {\n  const iso2 = (req.iso2 ?? '').trim().toUpperCase();\n\n  // Input-shape errors return 400 — restoring the legacy /api/supply-chain/v1/\n  // country-products contract which predated the sebuf migration. Empty-payload-200\n  // is reserved for the PRO-gate deny path (intentional contract shift), not for\n  // caller bugs (malformed/missing fields). Distinguishing the two matters for\n  // logging, external API consumers, and silent-failure detection.\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\n  const isPro = await isCallerPremium(ctx.request);\n  const empty: GetCountryProductsResponse = { iso2, products: [], fetchedAt: '' };\n  if (!isPro) return empty;\n\n  // Seeder writes via raw key (no env-prefix) — match it on read.\n  const key = `comtrade:bilateral-hs4:${iso2}:v1`;\n  const payload = await getCachedJson(key, true).catch(() => null) as BilateralHs4Payload | null;\n  if (!payload) return empty;\n\n  return {\n    iso2,\n    products: Array.isArray(payload.products) ? payload.products : [],\n    fetchedAt: payload.fetchedAt ?? '',\n  };\n}\n","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/supply-chain/v1/get-country-products.ts#L12-L48","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":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}