{"record":{"id":"cd4589985693cd00","repo":"koala73/worldmonitor","slug":"unsupported-airport-delay-parameter-key","errorCode":null,"errorMessage":"Unsupported airport delay parameter: ${key}","messagePattern":"Unsupported airport delay parameter: (.+?)","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/aviation/v1/list-airport-delays.ts","lineNumber":43,"sourceCode":"import { ApiError } from '../../../../src/generated/server/worldmonitor/aviation/v1/service_server';\n// @ts-expect-error — JS module, no declaration file\nimport { captureSilentError } from '../../../../api/_sentry-edge.js';\n\nconst FAA_CACHE_KEY = 'aviation:delays:faa:v1';\nconst INTL_CACHE_KEY = 'aviation:delays:intl:v3';\n\nconst FAA_AIRPORT_SET = new Set(FAA_AIRPORTS);\nconst INTL_AIRPORT_SET = new Set(AVIATIONSTACK_AIRPORTS);\n\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 }> => {","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/aviation/v1/list-airport-delays.ts#L25-L61","documentation":"listAirportDelays validates every query parameter in the request URL against ALLOWED_QUERY_PARAMS and throws ApiError 400 for any unrecognized key. The library accepts a strict allowlist (effectively 'page_size' and 'rpc' with exact required values) to catch client-side typos and stale parameters early. It also rejects duplicate parameters, non-'0' page_size values, and rpc values other than 'list-airport-delays'.","triggerScenarios":"Sending any query string key not in ALLOWED_QUERY_PARAMS, e.g. ?pageSize=0 (camelCase instead of snake_case), ?limit=10, ?airport=JFK, or appended cache-buster params like ?_=1690000000.","commonSituations":"Client builds the URL from a shared serializer that adds pagination params this endpoint does not support; a frontend adds cache-busting query params; migration from another delays endpoint with different parameter names; hand-typed URLs during debugging.","solutions":["Remove the unsupported query parameter and send only keys in ALLOWED_QUERY_PARAMS ('page_size' and 'rpc').","Fix parameter naming: use snake_case 'page_size', not 'pageSize' or 'limit'.","Stop appending cache-buster/timestamp query params to this RPC URL.","Call listAirportDelays through the request-object API (req.pageSize) instead of hand-building URLs, so the framework serializes only valid params."],"exampleFix":"// before\nconst url = `${base}/list-airport-delays?pageSize=0&_=${Date.now()}`;\n// after\nconst url = `${base}/list-airport-delays?page_size=0&rpc=list-airport-delays`;\n// or better: use the request object\nlistAirportDelays(ctx, { pageSize: 0 });","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['page_size', 'rpc']);\nfunction buildDelaysUrl(base, params = {}) {\n  const qs = new URLSearchParams();\n  for (const [k, v] of Object.entries(params)) {\n    if (!ALLOWED.has(k)) throw new Error(`Unsupported param: ${k}`);\n    qs.set(k, v);\n  }\n  return `${base}?${qs}`;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await listAirportDelays(ctx, req);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400 && e.message.startsWith('Unsupported')) {\n    // strip unknown params and retry once with the canonical URL\n    return listAirportDelays(ctx, { pageSize: 0 });\n  }\n  throw e;\n}","preventionTips":["Always build this URL from the typed request object, never by hand.","Use snake_case param names ('page_size') exactly as the server defines them.","Disable cache-buster query params on RPC endpoints.","Keep the client's allowlist in sync with server ALLOWED_QUERY_PARAMS."],"tags":["validation","query-parameters","api","aviation"],"backgroundTag":"invalid-query-parameter","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"}