{"record":{"id":"d51553500179169b","repo":"koala73/worldmonitor","slug":"must-be-a-lowercase-two-letter-language-code","errorCode":null,"errorMessage":"must be a lowercase two-letter language code","messagePattern":"must be a lowercase two-letter language code","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/news/v1/list-feed-digest.ts","lineNumber":1891,"sourceCode":"      level: LEVEL_TO_PROTO[item.level],\n      category: item.category,\n      confidence: item.confidence,\n      source: item.classSource,\n    },\n    locationName: '',\n    snippet: item.description ?? '',\n    tickers: item.tickers ?? [],\n  };\n}\n\nexport async function listFeedDigest(\n  ctx: ServerContext,\n  req: ListFeedDigestRequest,\n): Promise<ListFeedDigestResponse> {\n  const variant = VALID_VARIANTS.has(req.variant) ? req.variant : 'full';\n  const lang = req.lang === undefined || req.lang === '' ? 'en' : req.lang;\n  if (typeof lang !== 'string' || lang.length !== 2 || !/^[a-z]{2}$/.test(lang)) {\n    throw new ValidationError([{ field: 'lang', description: 'must be a lowercase two-letter language code' }]);\n  }\n\n  const digestCacheKey = `news:digest:v1:${variant}:${lang}`;\n  const fallbackKey = `${variant}:${lang}`;\n  const requestStart = Date.now();\n  const attemptedAt = new Date(requestStart).toISOString();\n  const responseDeadlineAt = requestStart + RESPONSE_DEADLINE_MS;\n  // Wall-clock budget for optional tail work. The build alone can consume\n  // ~19s worst case (14s fetcher timeout + a 5s sentinel write inside the\n  // cache wrapper); every awaited Redis op after it must fit inside the 25s\n  // Edge response ceiling minus a guard band.\n  // ONE revocation read per request, started at t=0 so its worst case\n  // overlaps the build instead of stacking after it. Shared by the fresh\n  // serve path and both replay tiers.\n  const revokedPromise = readRevokedUrlSet();\n\n  // #7085: an empty response still carries an explicit `unavailable`\n  // coverage block so clients can distinguish \"nothing served\" from","sourceCodeStart":1873,"sourceCodeEnd":1909,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/news/v1/list-feed-digest.ts#L1873-L1909","documentation":"listFeedDigest validates the req.lang parameter as a lowercase two-letter ISO language code. Values that are undefined/empty default to 'en', but any other value failing /^[a-z]{2}$/ (wrong length, uppercase, digits, non-string) throws this ValidationError naming the offending field.","triggerScenarios":"Calling listFeedDigest with lang like 'EN', 'eng', 'e', 'en-US', '12', or a non-string value; only exact two-char lowercase codes such as 'en', 'de', 'ja' pass.","commonSituations":"Clients sending BCP-47 tags ('en-US', 'pt-BR'), uppercase language codes from iOS locale identifiers, full language names ('english'), or accidentally passing the browser's full locale string instead of the 2-letter subtag.","solutions":["Send a lowercase two-letter ISO 639-1 code, e.g. lang='en'","Normalize client-side: locale.split('-')[0].toLowerCase() before calling the API","Omit lang (or pass empty string) to accept the 'en' default","If you need broader locales, extend the validation and cache key scheme in list-feed-digest to accept them"],"exampleFix":"// before\nawait api.listFeedDigest({ variant: 'brief', lang: 'en-US' });\n// after\nconst lang = 'en-US'.split('-')[0].toLowerCase();\nawait api.listFeedDigest({ variant: 'brief', lang });","handlingStrategy":"validation","validationCode":"const ISO_LANG = /^[a-z]{2}$/;\nif (lang !== undefined && lang !== '' && !(typeof lang === 'string' && ISO_LANG.test(lang))) {\n  throw new Error(\"lang must be a lowercase two-letter language code\");\n}","typeGuard":"function isLangCode(v: unknown): v is string {\n  return typeof v === 'string' && /^[a-z]{2}$/.test(v);\n}","tryCatchPattern":"try {\n  const digest = await listFeedDigest({ variant: 'full', lang });\n} catch (e) {\n  if (e instanceof ValidationError && e.issues?.some(i => i.field === 'lang')) {\n    digest = await listFeedDigest({ variant: 'full', lang: 'en' });\n  } else throw e;\n}","preventionTips":["Normalize locales to the 2-letter subtag client-side: locale.split('-')[0].toLowerCase()","Coerce to lowercase before sending ('EN' -> 'en')","Omit lang entirely when you want the 'en' default"],"tags":["validation","i18n","parameters"],"backgroundTag":"invalid-argument-format","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}