{"record":{"id":"7bd1ff13cdd23746","repo":"koala73/worldmonitor","slug":"airport-delay-filters-are-not-supported","errorCode":null,"errorMessage":"Airport delay filters are not supported","messagePattern":"Airport delay filters are not supported","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/aviation/v1/list-airport-delays.ts","lineNumber":52,"sourceCode":"\nconst ALLOWED_QUERY_PARAMS = new Set(['page_size', 'cursor', 'region', 'min_severity', 'jmespath', '_debug', 'rpc']);\n\nexport async function listAirportDelays(\n  ctx: ServerContext,\n  req: ListAirportDelaysRequest,\n): Promise<ListAirportDelaysResponse> {\n  const seenParams = new Set<string>();\n  for (const [key, value] of new URL(ctx.request.url).searchParams) {\n    if (!ALLOWED_QUERY_PARAMS.has(key)) throw new ApiError(400, `Unsupported airport delay parameter: ${key}`, '');\n    if (seenParams.has(key)) throw new ApiError(400, `Duplicate airport delay parameter: ${key}`, '');\n    seenParams.add(key);\n    if (key === 'page_size' && value !== '0') throw new ApiError(400, 'Airport delay page_size must be 0', '');\n    if (key === 'rpc' && value !== 'list-airport-delays') throw new ApiError(400, 'Invalid airport delay route', '');\n  }\n  if ((req.pageSize ?? 0) !== 0 || req.cursor\n    || (req.region && req.region !== 'AIRPORT_REGION_UNSPECIFIED')\n    || (req.minSeverity && req.minSeverity !== 'FLIGHT_DELAY_SEVERITY_UNSPECIFIED')) {\n    throw new ApiError(400, 'Airport delay filters are not supported', '');\n  }\n  // 1. FAA (US) — seed-only read\n  // faaSourceCovered = the seed cache hit AND returned a valid alerts array.\n  // A miss/parse-error means we have no telemetry for any FAA airport this\n  // tick — we MUST NOT publish synthetic \"normal\" rows for them. See #3707.\n  // PERF: the three inputs below are independent (different Redis keys / an\n  // independent fetcher) and merge only afterwards — start them concurrently\n  // instead of paying three serial round-trips per request.\n  const faaRead = (async (): Promise<{ faaAlerts: AirportDelayAlert[]; faaSourceCovered: boolean; available: boolean }> => {\n    let faaAlerts: AirportDelayAlert[] = [];\n    let faaSourceCovered = false;\n    try {\n      const seed = await readCachedJson(FAA_CACHE_KEY, true);\n      const seedData = seed.status === 'hit' ? seed.value as { alerts?: unknown[] } | null : null;\n      if (seedData && Array.isArray(seedData.alerts) && seedData.alerts.every(isValidAirportDelayAlert)) {\n        faaSourceCovered = true;\n        faaAlerts = seedData.alerts!\n          .map(a => {","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/aviation/v1/list-airport-delays.ts#L34-L70","documentation":"The seed-only airport delay listing supports no filtering: pageSize must be 0, no cursor, region must be AIRPORT_REGION_UNSPECIFIED (or unset), and minSeverity must be FLIGHT_DELAY_SEVERITY_UNSPECIFIED (or unset). Any filter combination throws ApiError 400.","triggerScenarios":"GET with region=AIRPORT_REGION_EU, minSeverity=<non-unspecified>, cursor=<token>, or a decoded proto request carrying pageSize>0 / cursor, alone or combined.","commonSituations":"Reusing client code written for filterable list endpoints; proto clients that populate default-valued enum fields explicitly; retry logic replaying a cursor from a prior paged response.","solutions":["Call the endpoint with no region, minSeverity, cursor, or page_size filters and filter client-side","Reset the proto message so unset/default enum values are not serialized onto the wire","Drop cursor-based pagination; re-fetch the full list instead of resuming","Send page_size=0 and leave all filter fields at their unspecified defaults"],"exampleFix":"// before\nconst req = { region: 'AIRPORT_REGION_EU', minSeverity: 'FLIGHT_DELAY_SEVERITY_MODERATE' };\n// after\nconst req = { region: 'AIRPORT_REGION_UNSPECIFIED', minSeverity: 'FLIGHT_DELAY_SEVERITY_UNSPECIFIED' };\n// then filter results in the client","handlingStrategy":"validation","validationCode":"const filterable = r => (r.pageSize ?? 0) === 0 && !r.cursor && (!r.region || r.region === 'AIRPORT_REGION_UNSPECIFIED') && (!r.minSeverity || r.minSeverity === 'FLIGHT_DELAY_SEVERITY_UNSPECIFIED');\nif (!filterable(req)) throw new Error('filters unsupported');","typeGuard":"const isUnfilteredDelayRequest = (r) => (r.pageSize ?? 0) === 0 && r.cursor == null && (r.region == null || r.region === 'AIRPORT_REGION_UNSPECIFIED') && (r.minSeverity == null || r.minSeverity === 'FLIGHT_DELAY_SEVERITY_UNSPECIFIED');","tryCatchPattern":"try { return await listAirportDelays(ctx, req); } catch (e) { if (e instanceof ApiError && e.message === 'Airport delay filters are not supported') { return fetchAllAndFilterClientSide(); } throw e; }","preventionTips":["Filter airport delay results client-side, never server-side","Reset proto enum fields to *_UNSPECIFIED instead of omitting only sometimes","Drop cursor reuse from paged endpoints when calling this one","Keep a typed request builder that forbids filter fields"],"tags":["http","validation","filters"],"backgroundTag":"unsupported-operation","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"}