{"record":{"id":"bc528c676360f41b","repo":"koala73/worldmonitor","slug":"countrycode-must-be-a-2-letter-iso-3166-1-alpha-2-bc528c","errorCode":null,"errorMessage":"countryCode must be a 2-letter ISO 3166-1 alpha-2 code","messagePattern":"countryCode must be a 2-letter ISO 3166-1 alpha-2 code","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/scorecard/v1/get-five-factor-scorecard.ts","lineNumber":21,"sourceCode":"  GetFiveFactorScorecardResponse,\n  ScorecardServiceHandler,\n  ServerContext,\n} from '../../../../src/generated/server/worldmonitor/scorecard/v1/service_server';\nimport { ValidationError } from '../../../../src/generated/server/worldmonitor/scorecard/v1/service_server';\n// @ts-expect-error — JS module, no declaration file\nimport { captureSilentError } from '../../../../api/_sentry-edge.js';\nimport { markNoStoreFallbackResponse } from '../../../_shared/response-headers';\nimport { asFiveFactorSnapshot, readFiveFactorSnapshot, type ScorecardSnapshotReader } from './_read-snapshot';\nimport { toPublicCountryScorecard } from './_response';\n\nexport async function getFiveFactorScorecardWithReader(\n  ctx: ServerContext,\n  req: GetFiveFactorScorecardRequest,\n  reader: ScorecardSnapshotReader,\n): Promise<GetFiveFactorScorecardResponse> {\n  const countryCode = String(req.countryCode || '').trim().toUpperCase();\n  if (!/^[A-Z]{2}$/.test(countryCode)) {\n    throw new ValidationError([{ field: 'countryCode', description: 'countryCode must be a 2-letter ISO 3166-1 alpha-2 code' }]);\n  }\n  let snapshotValue: unknown;\n  try {\n    snapshotValue = await reader([countryCode]);\n  } catch (error) {\n    console.warn('[scorecard] snapshot read failed operation=get-five-factor-scorecard', error instanceof Error ? error.message : 'unknown');\n    void captureSilentError(error, { tags: { route: 'scorecard/get-five-factor-scorecard', step: 'snapshot-read' } });\n    return markNoStoreFallbackResponse(ctx.request, {\n      unavailable: true,\n      unavailableReason: 'scorecard-snapshot-unavailable',\n    });\n  }\n  const snapshot = asFiveFactorSnapshot(snapshotValue);\n  const record = snapshot?.countries[countryCode];\n  if (!snapshot || !record) {\n    return markNoStoreFallbackResponse(ctx.request, {\n      unavailable: true,\n      unavailableReason: snapshot ? 'country-unavailable' : 'scorecard-snapshot-unavailable',","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/worldmonitor/scorecard/v1/get-five-factor-scorecard.ts#L3-L39","documentation":"getFiveFactorScorecard normalizes req.countryCode (trim + uppercase) and requires it to match /^[A-Z]{2}$/ before reading the scorecard snapshot. Anything else (empty, full country names, 3-letter codes, digits) throws this ValidationError on field 'countryCode'.","triggerScenarios":"Calling the five-factor scorecard endpoint with countryCode missing, empty string, 'USA', 'us' pre-normalization edge cases won't pass since it uppercases but length still matters, numeric input, or names like 'Germany'.","commonSituations":"Clients storing ISO-3 codes or display names; form inputs not constrained to 2 letters; query params arriving as numbers; regional codes like 'EU' that pass the regex but are not real countries (those pass validation and fail later at snapshot read).","solutions":["Send a 2-letter ISO 3166-1 alpha-2 code (e.g. 'DE', 'JP')","Trim and uppercase the input client-side before sending (server uppercases too, so 'de' works)","Validate with /^[A-Z]{2}$/ before the call","Map ISO-3 or country names to alpha-2 in the client before invoking"],"exampleFix":"// before\nawait getFiveFactorScorecard({ countryCode: 'USA' })\n// after\nconst code = String(raw).trim().toUpperCase();\nif (!/^[A-Z]{2}$/.test(code)) throw new Error('countryCode must be alpha-2');\nawait getFiveFactorScorecard({ countryCode: code })","handlingStrategy":"validation","validationCode":"function normalizeCountryCode(v) {\n  const code = String(v || '').trim().toUpperCase();\n  return /^[A-Z]{2}$/.test(code) ? code : null;\n}","typeGuard":"function isIso2(v) {\n  return typeof v === 'string' && /^[A-Z]{2}$/.test(v);\n}","tryCatchPattern":"try {\n  return await getFiveFactorScorecard({ countryCode });\n} catch (e) {\n  if (e.name === 'ValidationError' && e.field === 'countryCode') {\n    return null; // or re-prompt with e.description\n  }\n  throw e;\n}","preventionTips":["Store country codes as alpha-2 everywhere in the client","Constrain inputs with a 2-letter pattern or country picker","Normalize (trim + uppercase) at the boundary","Map alpha-3/names to alpha-2 before calling"],"tags":["validation","iso-3166","input-validation"],"backgroundTag":"invalid-request-parameter","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}