koala73/worldmonitor · warning · ValidationError

iso2 must be a 2-letter uppercase ISO country code

Error message

iso2 must be a 2-letter uppercase ISO country code

What it means

getCountryProducts trims and uppercases req.iso2, then requires ^[A-Z]{2}$ and throws a 400 ValidationError otherwise. The comment marks a deliberate contract split: input-shape errors return 400 (legacy /api/supply-chain/v1/country-products behavior), while the PRO-gate deny path returns an empty 200 — so a 400 here always means a caller bug, not a permissions issue.

Source

Thrown at server/worldmonitor/supply-chain/v1/get-country-products.ts:30

interface BilateralHs4Payload {
  iso2: string;
  products?: CountryProduct[];
  fetchedAt?: string;
}

export async function getCountryProducts(
  ctx: ServerContext,
  req: GetCountryProductsRequest,
): Promise<GetCountryProductsResponse> {
  const iso2 = (req.iso2 ?? '').trim().toUpperCase();

  // Input-shape errors return 400 — restoring the legacy /api/supply-chain/v1/
  // country-products contract which predated the sebuf migration. Empty-payload-200
  // is reserved for the PRO-gate deny path (intentional contract shift), not for
  // caller bugs (malformed/missing fields). Distinguishing the two matters for
  // logging, external API consumers, and silent-failure detection.
  if (!/^[A-Z]{2}$/.test(iso2)) {
    throw new ValidationError([{ field: 'iso2', description: 'iso2 must be a 2-letter uppercase ISO country code' }]);
  }

  const isPro = await isCallerPremium(ctx.request);
  const empty: GetCountryProductsResponse = { iso2, products: [], fetchedAt: '' };
  if (!isPro) return empty;

  // Seeder writes via raw key (no env-prefix) — match it on read.
  const key = `comtrade:bilateral-hs4:${iso2}:v1`;
  const payload = await getCachedJson(key, true).catch(() => null) as BilateralHs4Payload | null;
  if (!payload) return empty;

  return {
    iso2,
    products: Array.isArray(payload.products) ? payload.products : [],
    fetchedAt: payload.fetchedAt ?? '',
  };
}

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Send exactly 2 letters (ISO-3166-1 alpha-2), e.g. 'CN', after trim + toUpperCase
  2. Validate client-side with /^[A-Z]{2}$/ after uppercasing before the RPC
  3. Source codes from a fixed country list/dropdown instead of free text

Example fix

// before
getCountryProducts(ctx, { iso2: 'china' });
// after
getCountryProducts(ctx, { iso2: 'CN' });
Defensive patterns

Strategy: validation

Validate before calling

const iso2 = value.trim().toUpperCase(); if (!/^[A-Z]{2}$/.test(iso2)) throw new RangeError('iso2 must be a 2-letter ISO-3166-1 alpha-2 code');

Type guard

const isIso2 = (v: unknown): v is string => typeof v === 'string' && /^[A-Z]{2}$/.test(v.trim().toUpperCase());

Try / catch

catch (e) { if (e?.details?.[0]?.field === 'iso2') { normalize to alpha-2 and re-submit } else throw e; }

Prevention

When it happens

Trigger: Calling GetCountryProducts with iso2 missing, empty, 1 or 3+ characters, or containing non-letters ('usa', 'U', 'DEU', 'D1'). Lowercase 'de' passes because normalization uppercases first; only post-normalization failures throw.

Common situations: Passing ISO3 codes or country names from UI text inputs; empty string from an unset query param; client defaulting to undefined before the call; migration from an API that accepted 3-letter codes.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/65c20850c93a1036. Report an issue: GitHub.