koala73/worldmonitor · warning · ValidationError

must be a lowercase two-letter language code

Error message

must be a lowercase two-letter language code

What it means

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.

Solutions

  1. Send a lowercase two-letter ISO 639-1 code, e.g. lang='en'
  2. Normalize client-side: locale.split('-')[0].toLowerCase() before calling the API
  3. Omit lang (or pass empty string) to accept the 'en' default
  4. If you need broader locales, extend the validation and cache key scheme in list-feed-digest to accept them

Example fix

// before
await api.listFeedDigest({ variant: 'brief', lang: 'en-US' });
// after
const lang = 'en-US'.split('-')[0].toLowerCase();
await api.listFeedDigest({ variant: 'brief', lang });
Defensive patterns

Strategy: validation

Validate before calling

const ISO_LANG = /^[a-z]{2}$/;
if (lang !== undefined && lang !== '' && !(typeof lang === 'string' && ISO_LANG.test(lang))) {
  throw new Error("lang must be a lowercase two-letter language code");
}

Type guard

function isLangCode(v: unknown): v is string {
  return typeof v === 'string' && /^[a-z]{2}$/.test(v);
}

Try / catch

try {
  const digest = await listFeedDigest({ variant: 'full', lang });
} catch (e) {
  if (e instanceof ValidationError && e.issues?.some(i => i.field === 'lang')) {
    digest = await listFeedDigest({ variant: 'full', lang: 'en' });
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/d51553500179169b. Report an issue: GitHub.

Appendix: source

Thrown at server/worldmonitor/news/v1/list-feed-digest.ts:1891

      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' || lang.length !== 2 || !/^[a-z]{2}$/.test(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 7d06c8633d)