koala73/worldmonitor · error · ApiError

Expected three-letter IATA airport codes

Error message

Expected three-letter IATA airport codes

What it means

getAirportOpsSummary normalizes each code with trim().toUpperCase() and rejects any entry that does not match IATA_RE (three-letter IATA format), throwing ApiError 400. The library requires exactly three alphabetic IATA airport codes; whitespace-only trimming and casing fixes are applied first, so anything still non-conforming is caller error. An empty list is allowed and falls back to DEFAULT_WATCHED_AIRPORTS.

Solutions

  1. Replace invalid entries with proper three-letter IATA codes (e.g. 'KJFK' -> 'JFK').
  2. Pre-filter the list: keep only codes matching /^[A-Z]{3}$/ after trim/uppercase.
  3. Deduplicate and remove empty strings before sending (empty list is valid and uses defaults).
  4. If ICAO codes are what you have, map them to IATA via a lookup table before the call.

Example fix

// before
getAirportOpsSummary(ctx, { airports: ['JFK', 'KJFK', ''] });
// after
const valid = ['JFK', 'KJFK', '']
  .map(c => c.trim().toUpperCase())
  .filter(c => /^[A-Z]{3}$/.test(c));
getAirportOpsSummary(ctx, { airports: valid });
Defensive patterns

Strategy: validation

Validate before calling

const IATA = /^[A-Z]{3}$/;
const clean = codes => codes
  .map(c => String(c).trim().toUpperCase())
  .filter(c => IATA.test(c));
// call with clean(codes) so only valid IATA codes reach the API

Type guard

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

Try / catch

try {
  return await getAirportOpsSummary(ctx, { airports: codes });
} catch (e) {
  if (e instanceof ApiError && e.message.includes('three-letter')) {
    return getAirportOpsSummary(ctx, { airports: codes.filter(isIataCode) });
  }
  throw e;
}

Prevention

When it happens

Trigger: Any airport code that after trim/uppercase is not exactly three letters matching IATA_RE: empty strings, 2- or 4-letter codes, digits or symbols ('JFK1', 'JK', ''), or non-string junk that parseStringArray coerced into a bad entry.

Common situations: Users type city names or ICAO codes (4 letters, e.g. 'KJFK') instead of IATA codes; client passes empty strings from a split on trailing commas; locale data contains lowercase-with-punctuation entries that fail the regex.

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@e586b8b4b8 (2026-09-22). Data as JSON: /api/errors/f0b62fa144bf11cc. Report an issue: GitHub.

Appendix: source

Thrown at server/worldmonitor/aviation/v1/get-airport-ops-summary.ts:47

const AVIATIONSTACK_AIRPORT_SET = new Set(AVIATIONSTACK_AIRPORTS);
export async function getAirportOpsSummary(
    ctx: ServerContext,
    req: GetAirportOpsSummaryRequest,
): Promise<GetAirportOpsSummaryResponse> {
    const raw: unknown = req.airports;
    if (raw != null && !(typeof raw === 'string'
        ? raw.length <= MAX_AIRPORT_INPUT_LENGTH
        : Array.isArray(raw) && raw.length <= MAX_OPS_AIRPORTS
            && raw.every(code => typeof code === 'string' && code.length <= MAX_AIRPORT_INPUT_LENGTH))) {
        throw new ApiError(400, 'Expected at most 20 IATA airport codes', '');
    }
    const rawAirports = parseStringArray(raw);
    if (rawAirports.length > MAX_OPS_AIRPORTS) {
        throw new ApiError(400, 'Expected at most 20 IATA airport codes', '');
    }
    const normalized = rawAirports.map(code => code.trim().toUpperCase());
    if (normalized.some(code => !IATA_RE.test(code))) {
        throw new ApiError(400, 'Expected three-letter IATA airport codes', '');
    }
    const requested = normalized.length > 0
        ? [...new Set(normalized)]
        : DEFAULT_WATCHED_AIRPORTS;

    const now = Date.now();

    try {
        const airports = MONITORED_AIRPORTS.filter(a => requested.includes(a.iata));
        const summaries: AirportOpsSummary[] = [];

        const notamRead = loadNotamClosures();
        let alerts: AirportDelayAlert[] = [];
        let healthy = false;
        // Per-hub coverage the seeder recorded this tick (see #3707's fix to the
        // sibling list-airport-delays route). A hit on SEED_CACHE_KEY only proves
        // *some* hubs came back healthy, not that every requested airport did —
        // and airports outside AVIATIONSTACK_AIRPORTS entirely (e.g. ESB, SAW)

View on GitHub (pinned to e586b8b4b8)