{"record":{"id":"f0b62fa144bf11cc","repo":"koala73/worldmonitor","slug":"expected-three-letter-iata-airport-codes","errorCode":null,"errorMessage":"Expected three-letter IATA airport codes","messagePattern":"Expected three-letter IATA airport codes","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/aviation/v1/get-airport-ops-summary.ts","lineNumber":47,"sourceCode":"const AVIATIONSTACK_AIRPORT_SET = new Set(AVIATIONSTACK_AIRPORTS);\nexport async function getAirportOpsSummary(\n    ctx: ServerContext,\n    req: GetAirportOpsSummaryRequest,\n): Promise<GetAirportOpsSummaryResponse> {\n    const raw: unknown = req.airports;\n    if (raw != null && !(typeof raw === 'string'\n        ? raw.length <= MAX_AIRPORT_INPUT_LENGTH\n        : Array.isArray(raw) && raw.length <= MAX_OPS_AIRPORTS\n            && raw.every(code => typeof code === 'string' && code.length <= MAX_AIRPORT_INPUT_LENGTH))) {\n        throw new ApiError(400, 'Expected at most 20 IATA airport codes', '');\n    }\n    const rawAirports = parseStringArray(raw);\n    if (rawAirports.length > MAX_OPS_AIRPORTS) {\n        throw new ApiError(400, 'Expected at most 20 IATA airport codes', '');\n    }\n    const normalized = rawAirports.map(code => code.trim().toUpperCase());\n    if (normalized.some(code => !IATA_RE.test(code))) {\n        throw new ApiError(400, 'Expected three-letter IATA airport codes', '');\n    }\n    const requested = normalized.length > 0\n        ? [...new Set(normalized)]\n        : DEFAULT_WATCHED_AIRPORTS;\n\n    const now = Date.now();\n\n    try {\n        const airports = MONITORED_AIRPORTS.filter(a => requested.includes(a.iata));\n        const summaries: AirportOpsSummary[] = [];\n\n        const notamRead = loadNotamClosures();\n        let alerts: AirportDelayAlert[] = [];\n        let healthy = false;\n        // Per-hub coverage the seeder recorded this tick (see #3707's fix to the\n        // sibling list-airport-delays route). A hit on SEED_CACHE_KEY only proves\n        // *some* hubs came back healthy, not that every requested airport did —\n        // and airports outside AVIATIONSTACK_AIRPORTS entirely (e.g. ESB, SAW)","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/aviation/v1/get-airport-ops-summary.ts#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ngetAirportOpsSummary(ctx, { airports: ['JFK', 'KJFK', ''] });\n// after\nconst valid = ['JFK', 'KJFK', '']\n  .map(c => c.trim().toUpperCase())\n  .filter(c => /^[A-Z]{3}$/.test(c));\ngetAirportOpsSummary(ctx, { airports: valid });","handlingStrategy":"validation","validationCode":"const IATA = /^[A-Z]{3}$/;\nconst clean = codes => codes\n  .map(c => String(c).trim().toUpperCase())\n  .filter(c => IATA.test(c));\n// call with clean(codes) so only valid IATA codes reach the API","typeGuard":"const isIataCode = (c: unknown): c is string =>\n  typeof c === 'string' && /^[A-Z]{3}$/.test(c.trim().toUpperCase());","tryCatchPattern":"try {\n  return await getAirportOpsSummary(ctx, { airports: codes });\n} catch (e) {\n  if (e instanceof ApiError && e.message.includes('three-letter')) {\n    return getAirportOpsSummary(ctx, { airports: codes.filter(isIataCode) });\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["validation","format","aviation","iata"],"backgroundTag":"invalid-argument-format","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}