koala73/worldmonitor · warning · ValidationError

fromIso2 and toIso2 must be valid 2-letter ISO country codes

Error message

fromIso2 and toIso2 must be valid 2-letter ISO country codes

What it means

routeIntelligence trims and uppercases fromIso2/toIso2, then requires both to match ^[A-Z]{2}$ and throws a 400 on the fromIso2 field (one message covers both). It fires after the PRO gate and before the COUNTRY_PORT_CLUSTERS lookup, so an invalid pair never reaches routing logic.

Source

Thrown at server/worldmonitor/shipping/v2/route-intelligence.ts:50

}

interface ChokepointStatusResponse {
  chokepoints?: ChokepointStatusEntry[];
  upstreamUnavailable?: boolean;
}

const VALID_CARGO_TYPES = new Set(['container', 'tanker', 'bulk', 'roro']);

export async function routeIntelligence(
  ctx: ServerContext,
  req: RouteIntelligenceRequest,
): Promise<RouteIntelligenceResponse> {
  await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');

  const fromIso2 = (req.fromIso2 ?? '').trim().toUpperCase();
  const toIso2 = (req.toIso2 ?? '').trim().toUpperCase();
  if (!/^[A-Z]{2}$/.test(fromIso2) || !/^[A-Z]{2}$/.test(toIso2)) {
    throw new ValidationError([
      { field: 'fromIso2', description: 'fromIso2 and toIso2 must be valid 2-letter ISO country codes' },
    ]);
  }

  const cargoTypeRaw = (req.cargoType ?? '').trim().toLowerCase();
  const cargoType: CargoType = (VALID_CARGO_TYPES.has(cargoTypeRaw) ? cargoTypeRaw : 'container') as CargoType;
  const hs2 = (req.hs2 ?? '').trim().replace(/\D/g, '') || '27';

  const clusters = COUNTRY_PORT_CLUSTERS as unknown as Record<string, PortClusterEntry>;
  const fromCluster = clusters[fromIso2];
  const toCluster = clusters[toIso2];

  const fromRoutes = new Set(fromCluster?.nearestRouteIds ?? []);
  const toRoutes = new Set(toCluster?.nearestRouteIds ?? []);
  const sharedRoutes = [...fromRoutes].filter(r => toRoutes.has(r));
  const primaryRouteId = sharedRoutes[0] ?? fromCluster?.nearestRouteIds[0] ?? '';

  const statusRaw = (await getCachedJson(CHOKEPOINT_STATUS_KEY).catch(() => null)) as ChokepointStatusResponse | null;

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Send 2-letter ISO-3166-1 alpha-2 codes for both fromIso2 and toIso2
  2. Constrain client input to a country selector keyed by alpha-2 codes
  3. Normalize client-side (trim + toUpperCase) and validate against /^[A-Z]{2}$/ before sending

Example fix

// before
routeIntelligence(ctx, { fromIso2: 'usa', toIso2: 'JPN' });
// after
routeIntelligence(ctx, { fromIso2: 'US', toIso2: 'JP' });
Defensive patterns

Strategy: validation

Validate before calling

const norm = (v: string) => v.trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(norm(from)) || !/^[A-Z]{2}$/.test(norm(to))) throw new RangeError('fromIso2/toIso2 must be ISO-3166-1 alpha-2');

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 === 'fromIso2') { re-check both codes against ISO-3166-1 alpha-2 and re-submit } else throw e; }

Prevention

When it happens

Trigger: Calling RouteIntelligence with fromIso2 or toIso2 missing, empty, 1 or 3+ characters, or containing digits/punctuation ('usa', 'U', 'US1', 'U.S.'). Lowercase input like 'us' is fine because the handler uppercases before testing — only post-normalization failures throw.

Common situations: Free-text country input not constrained to ISO-3166-1 alpha-2; clients sending ISO3 ('USA') or numeric M49 codes; passing a full country name from a form field.

Related errors


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