{"record":{"id":"b4d88c64dadb00b6","repo":"koala73/worldmonitor","slug":"expected-at-most-20-iata-airport-codes","errorCode":null,"errorMessage":"Expected at most 20 IATA airport codes","messagePattern":"Expected at most 20 IATA airport codes","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/aviation/v1/get-airport-ops-summary.ts","lineNumber":39,"sourceCode":"    isValidIntlCoverage,\n    loadNotamClosures,\n    IATA_RE,\n} from './_shared';\n\nconst SEED_CACHE_KEY = 'aviation:delays:intl:v3';\nconst MAX_OPS_AIRPORTS = 20; // get_airport_ops_summary.proto repeated.max_items\nconst MAX_AIRPORT_INPUT_LENGTH = 1024;\nconst 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[] = [];","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/aviation/v1/get-airport-ops-summary.ts#L21-L57","documentation":"getAirportOpsSummary validates req.airports before use and throws ApiError 400 when the input clearly exceeds the 20-code limit or individual entries are too long. The library enforces MAX_OPS_AIRPORTS (20) and MAX_AIRPORT_INPUT_LENGTH to cap request size. A string input longer than MAX_AIRPORT_INPUT_LENGTH, or an array with more than 20 entries or entries exceeding the per-code length cap, fails this first guard.","triggerScenarios":"Calling getAirportOpsSummary with req.airports as a string longer than MAX_AIRPORT_INPUT_LENGTH, or as an array with more than 20 elements, or an array containing a non-string element or a string longer than MAX_AIRPORT_INPUT_LENGTH.","commonSituations":"A client joins codes into one long comma string instead of an array, a batch job passes hundreds of airports at once, or a caller forwards unvalidated query params straight into the request object.","solutions":["Reduce the airports input to at most 20 IATA codes per request.","Pass airports as an array of short strings rather than one concatenated long string.","Validate/slice the list client-side before constructing ListAirportDelaysRequest-style airport-ops requests.","Paginate or chunk large airport watchlists into batches of 20 with multiple calls."],"exampleFix":"// before\ngetAirportOpsSummary(ctx, { airports: allAirports }); // allAirports.length === 120\n// after\nconst batch = allAirports.slice(0, 20).map(c => c.trim().toUpperCase());\nif (batch.some(c => c.length > 3)) throw new Error('invalid airport code length');\ngetAirportOpsSummary(ctx, { airports: batch });","handlingStrategy":"validation","validationCode":"function canCallAirportOps(codes) {\n  return Array.isArray(codes)\n    && codes.length <= 20\n    && codes.every(c => typeof c === 'string' && c.length <= 3);\n}","typeGuard":"function isAirportOpsInput(raw) {\n  if (typeof raw === 'string') return raw.length <= 3;\n  return Array.isArray(raw) && raw.length <= 20\n    && raw.every(c => typeof c === 'string' && c.length <= 3);\n}","tryCatchPattern":"try {\n  return await getAirportOpsSummary(ctx, req);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400) {\n    return emptyOpsSummary(req); // or rethrow after chunking\n  }\n  throw e;\n}","preventionTips":["Always slice or chunk airport lists to <= 20 before calls.","Keep codes as arrays, not concatenated strings.","Share a single client-side validation constant (20) with the server limit.","Add unit tests for boundary sizes 20 and 21."],"tags":["validation","api","aviation","request-limits"],"backgroundTag":"value-out-of-range","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"}