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

  1. Send lang as a lowercase two-letter code from DIGEST_LANGUAGES (e.g. 'en', 'de') or omit it to default to 'en'.
  2. Normalize the client locale before calling: take the primary subtag and lowercase it.
  3. Check DIGEST_LANGUAGES in list-feed-digest.ts for the exact supported set and map unsupported locales to a supported one.
  4. 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

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


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" from

View on GitHub (pinned to e586b8b4b8)