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
- Replace invalid entries with proper three-letter IATA codes (e.g. 'KJFK' -> 'JFK').
- Pre-filter the list: keep only codes matching /^[A-Z]{3}$/ after trim/uppercase.
- Deduplicate and remove empty strings before sending (empty list is valid and uses defaults).
- 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
- Validate codes with /^[A-Z]{3}$/ on input, server-style trim/uppercase included.
- Convert ICAO (4-letter) codes to IATA via a lookup before sending.
- Strip empty strings produced by split(',') on trailing commas.
- Keep an allowlist of supported airports in the client config.
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
- Expected a six-character hexadecimal ICAO address
- Expected an alphanumeric callsign of at most eight…
- Aircraft identifier is too long
- Custom scorecard bloc members must be uppercase ISO-2 codes.
- Dynamic ApiError(400, message) via local invalid() helper…
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)