koala73/worldmonitor · error · ApiError
Duplicate airport delay parameter
Error message
Duplicate airport delay parameter: ${key} What it means
listAirportDelays rejects any query parameter repeated on the request URL. The handler iterates URL searchParams with a seenParams Set and throws ApiError 400 as soon as a key appears a second time, since repeated params are ambiguous for a seed-only listing endpoint.
Solutions
- Remove the duplicated query parameter so each key appears at most once in the URL
- Fix the client's query-string builder to deduplicate keys instead of appending per array item
- Log ctx.request.url on failure to identify which key is repeated and which code path adds it
- Add a client-side test asserting the generated query string has unique keys
Example fix
// before
const params = new URLSearchParams();
regions.forEach(r => params.append('rpc', 'list-airport-delays'));
// after
const params = new URLSearchParams({ rpc: 'list-airport-delays' }); Defensive patterns
Strategy: validation
Validate before calling
const keys = [...new URL(url).searchParams.keys()];
if (new Set(keys).size !== keys.length) throw new Error('duplicate query param'); Type guard
const hasUniqueParams = (url) => { const ks = [...new URL(url).searchParams.keys()]; return new Set(ks).size === ks.length; }; Try / catch
try { await listAirportDelays(ctx, req); } catch (e) { if (e instanceof ApiError && e.status === 400 && /Duplicate .* parameter/.test(e.message)) { rebuildQueryUnique(); } else throw e; } Prevention
- Build query strings from plain objects, not repeated append calls
- Never serialize array values as repeated query keys for this endpoint
- Add a unit test asserting unique query keys
- Deduplicate params in any fetch wrapper before sending
When it happens
Trigger: Calling GET .../list-airport-delays with the same query key twice, e.g. ?rpc=list-airport-delays&rpc=list-airport-delays or ?page_size=0&page_size=0 (including arrays serialized as key=a&key=b).
Common situations: Client code building query strings from objects/arrays where a value is a list; URLSearchParams.append called twice; framework serializers that emit repeated keys for array values; proxies rewriting and re-appending params.
Related errors
- Airport delay filters are not supported
- Airport delay page_size must be 0
- deadlineMs must be a positive number
- Expected finite viewport coordinates
- Invalid airport delay route
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/038bddb4507e1d93.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/aviation/v1/list-airport-delays.ts:44
// @ts-expect-error — JS module, no declaration file
import { captureSilentError } from '../../../../api/_sentry-edge.js';
const FAA_CACHE_KEY = 'aviation:delays:faa:v1';
const INTL_CACHE_KEY = 'aviation:delays:intl:v3';
const FAA_AIRPORT_SET = new Set(FAA_AIRPORTS);
const INTL_AIRPORT_SET = new Set(AVIATIONSTACK_AIRPORTS);
const ALLOWED_QUERY_PARAMS = new Set(['page_size', 'cursor', 'region', 'min_severity', 'jmespath', '_debug', 'rpc']);
export async function listAirportDelays(
ctx: ServerContext,
req: ListAirportDelaysRequest,
): Promise<ListAirportDelaysResponse> {
const seenParams = new Set<string>();
for (const [key, value] of new URL(ctx.request.url).searchParams) {
if (!ALLOWED_QUERY_PARAMS.has(key)) throw new ApiError(400, `Unsupported airport delay parameter: ${key}`, '');
if (seenParams.has(key)) throw new ApiError(400, `Duplicate airport delay parameter: ${key}`, '');
seenParams.add(key);
if (key === 'page_size' && value !== '0') throw new ApiError(400, 'Airport delay page_size must be 0', '');
if (key === 'rpc' && value !== 'list-airport-delays') throw new ApiError(400, 'Invalid airport delay route', '');
}
if ((req.pageSize ?? 0) !== 0 || req.cursor
|| (req.region && req.region !== 'AIRPORT_REGION_UNSPECIFIED')
|| (req.minSeverity && req.minSeverity !== 'FLIGHT_DELAY_SEVERITY_UNSPECIFIED')) {
throw new ApiError(400, 'Airport delay filters are not supported', '');
}
// 1. FAA (US) — seed-only read
// faaSourceCovered = the seed cache hit AND returned a valid alerts array.
// A miss/parse-error means we have no telemetry for any FAA airport this
// tick — we MUST NOT publish synthetic "normal" rows for them. See #3707.
// PERF: the three inputs below are independent (different Redis keys / an
// independent fetcher) and merge only afterwards — start them concurrently
// instead of paying three serial round-trips per request.
const faaRead = (async (): Promise<{ faaAlerts: AirportDelayAlert[]; faaSourceCovered: boolean; available: boolean }> => {
let faaAlerts: AirportDelayAlert[] = [];View on GitHub (pinned to e586b8b4b8)