{"record":{"id":"ee88d374de3bd05c","repo":"koala73/worldmonitor","slug":"validation-failed","errorCode":null,"errorMessage":"Validation failed","messagePattern":"Validation failed","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"warning","filePath":"server/worldmonitor/news/v1/list-feed-digest.ts","lineNumber":1950,"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' || !DIGEST_LANGUAGES.has(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":1932,"sourceCodeEnd":1968,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/news/v1/list-feed-digest.ts#L1932-L1968","documentation":"ValidationError thrown by the ListFeedDigest handler when the req.lang field is not one of the supported digest languages. The handler defaults missing lang to 'en', but an explicit non-string or unsupported language code fails validation with a 400-style structured error listing field 'lang'.","triggerScenarios":"Calling ListFeedDigest with lang set to a value not in DIGEST_LANGUAGES — e.g. 'EN' (uppercase), 'en-US', 'fra', or a two-letter code the digest doesn't support.","commonSituations":"Clients passing locale strings (BCP-47 tags like 'pt-BR') instead of the expected lowercase two-letter codes; uppercase codes from UI locale headers; a language recently removed from DIGEST_LANGUAGES still being requested by an older client.","solutions":["Send lang as a lowercase two-letter code from DIGEST_LANGUAGES (e.g. 'en', 'de') or omit it to default to 'en'.","Normalize the client locale before calling: take the primary subtag and lowercase it.","Check DIGEST_LANGUAGES in list-feed-digest.ts for the exact supported set and map unsupported locales to a supported one.","If a needed language is missing, add it to DIGEST_LANGUAGES and ensure a digest exists for it."],"exampleFix":"// before: passes raw browser locale\nawait listFeedDigest(ctx, { variant: 'full', lang: navigator.language }); // \"en-US\"\n// after: normalize to the expected two-letter code\nconst lang = navigator.language.slice(0, 2).toLowerCase();\nawait listFeedDigest(ctx, { variant: 'full', lang });","handlingStrategy":"validation","validationCode":"const SUPPORTED = new Set(['en','de','fr','es']); // mirror DIGEST_LANGUAGES\nfunction normalizeLang(l) {\n  const code = String(l ?? '').split('-')[0].toLowerCase();\n  return SUPPORTED.has(code) ? code : 'en';\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await listFeedDigest(ctx, { variant, lang });\n} catch (e) {\n  if (isValidationError(e) && e.issues?.some(i => i.field === 'lang')) {\n    return listFeedDigest(ctx, { variant, lang: 'en' });\n  }\n  throw e;\n}","preventionTips":["Always normalize locale strings to lowercase two-letter codes before calling.","Keep the client-side supported-language list in sync with DIGEST_LANGUAGES.","Omit lang entirely when you want the default rather than guessing a code."],"tags":["validation","api","i18n"],"backgroundTag":"invalid-argument-value","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}