koala73/worldmonitor · warning · ValidationError
Validation failed
Error message
Validation failed
What it means
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'.
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.
Example fix
// before: passes raw browser locale
await listFeedDigest(ctx, { variant: 'full', lang: navigator.language }); // "en-US"
// after: normalize to the expected two-letter code
const lang = navigator.language.slice(0, 2).toLowerCase();
await listFeedDigest(ctx, { variant: 'full', lang }); Defensive patterns
Strategy: validation
Validate before calling
const SUPPORTED = new Set(['en','de','fr','es']); // mirror DIGEST_LANGUAGES
function normalizeLang(l) {
const code = String(l ?? '').split('-')[0].toLowerCase();
return SUPPORTED.has(code) ? code : 'en';
} Try / catch
try {
return await listFeedDigest(ctx, { variant, lang });
} catch (e) {
if (isValidationError(e) && e.issues?.some(i => i.field === 'lang')) {
return listFeedDigest(ctx, { variant, lang: 'en' });
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- disruptionPct must be an integer from 0 to 100 for a…
- EONET malformed event
- Expected a supported two-letter market code
- Expected an array of Sentry issues. A Sentry error body is…
- Expected at most 20 IATA airport codes
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/ee88d374de3bd05c.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/news/v1/list-feed-digest.ts:1950
level: LEVEL_TO_PROTO[item.level],
category: item.category,
confidence: item.confidence,
source: item.classSource,
},
locationName: '',
snippet: item.description ?? '',
tickers: item.tickers ?? [],
};
}
export async function listFeedDigest(
ctx: ServerContext,
req: ListFeedDigestRequest,
): Promise<ListFeedDigestResponse> {
const variant = VALID_VARIANTS.has(req.variant) ? req.variant : 'full';
const lang = req.lang === undefined || req.lang === '' ? 'en' : req.lang;
if (typeof lang !== 'string' || !DIGEST_LANGUAGES.has(lang)) {
throw new ValidationError([{ field: 'lang', description: 'must be a lowercase two-letter language code' }]);
}
const digestCacheKey = `news:digest:v1:${variant}:${lang}`;
const fallbackKey = `${variant}:${lang}`;
const requestStart = Date.now();
const attemptedAt = new Date(requestStart).toISOString();
const responseDeadlineAt = requestStart + RESPONSE_DEADLINE_MS;
// Wall-clock budget for optional tail work. The build alone can consume
// ~19s worst case (14s fetcher timeout + a 5s sentinel write inside the
// cache wrapper); every awaited Redis op after it must fit inside the 25s
// Edge response ceiling minus a guard band.
// ONE revocation read per request, started at t=0 so its worst case
// overlaps the build instead of stacking after it. Shared by the fresh
// serve path and both replay tiers.
const revokedPromise = readRevokedUrlSet();
// #7085: an empty response still carries an explicit `unavailable`
// coverage block so clients can distinguish "nothing served" fromView on GitHub (pinned to e586b8b4b8)