koala73/worldmonitor · error · ApiError
Expected finite viewport coordinates
Error message
Expected finite viewport coordinates
What it means
trackAircraft requires all four viewport fields (swLat, swLon, neLat, neLon) to be finite numbers. If any is missing, null, or non-numeric (NaN/Infinity/string), it throws ApiError 400 before clamping coordinates into valid lat/lon ranges.
Solutions
- Validate all four coordinates are finite numbers before calling (Number.isFinite on each)
- Only invoke trackAircraft after the map viewport/bounds are initialized
- Coerce numeric strings with Number() and reject NaN prior to the request
- Check the client serialization so numbers are not sent as strings
Example fix
// before
await trackAircraft({ icao24, swLat: bounds?.swLat, ... });
// after
const { swLat, swLon, neLat, neLon } = bounds;
if (![swLat, swLon, neLat, neLon].every(Number.isFinite)) throw new Error('viewport not ready');
await trackAircraft({ icao24, swLat, swLon, neLat, neLon }); Defensive patterns
Strategy: validation
Validate before calling
if (![swLat, swLon, neLat, neLon].every(Number.isFinite)) throw new Error('viewport not ready'); Type guard
const hasValidViewport = (r) => [r.swLat, r.swLon, r.neLat, r.neLon].every(n => typeof n === 'number' && Number.isFinite(n));
Try / catch
try { await trackAircraft(req); } catch (e) { if (e instanceof ApiError && e.status === 400 && e.message.includes('finite viewport')) { await waitForMapReady(); return trackAircraft(req); } throw e; } Prevention
- Gate map-backed calls on the map's bounds-ready event
- Coerce inputs with Number() and check Number.isFinite before sending
- Make viewport fields non-optional in the client request type
- Sanitize NaN/Infinity early at the state layer
When it happens
Trigger: Calling track-aircraft with an omitted viewport field, JSON body where a coordinate is a string like "45.5", NaN/Infinity from a previous computation, or null from an unset map viewport.
Common situations: Map component not yet initialized so its bounds are null; parse failures producing NaN; API clients omitting optional-looking fields the server actually requires.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Airport delay filters are not supported
- Airport delay page_size must be 0
- deadlineMs must be a positive number
- Duplicate airport delay parameter
- Invalid airport delay route
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/9ec361729202b3ec.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/aviation/v1/track-aircraft.ts:124
return 'aviation:track:all:v2';
}
// Response-level source values (TrackAircraftResponse.source):
// 'opensky' — data from OpenSky via relay
// 'wingbits' — data from Wingbits via relay
// 'none' — all real sources returned empty or failed; positions = []
export async function trackAircraft(
ctx: ServerContext,
req: TrackAircraftRequest,
): Promise<TrackAircraftResponse> {
const rawIcao24 = req.icao24 ?? '';
const rawCallsign = req.callsign ?? '';
if (rawIcao24.length > 16 || rawCallsign.length > 16) throw new ApiError(400, 'Aircraft identifier is too long', '');
const icao24 = rawIcao24.trim().toLowerCase();
const callsign = rawCallsign.trim().toUpperCase();
if (rawIcao24 && !/^[0-9a-f]{6}$/.test(icao24)) throw new ApiError(400, 'Expected a six-character hexadecimal ICAO address', '');
if (rawCallsign && !/^[A-Z0-9]{1,8}$/.test(callsign)) throw new ApiError(400, 'Expected an alphanumeric callsign of at most eight characters', '');
if (![req.swLat, req.swLon, req.neLat, req.neLon].every(Number.isFinite)) throw new ApiError(400, 'Expected finite viewport coordinates', '');
const lat1 = Math.max(-90, Math.min(90, req.swLat));
const lat2 = Math.max(-90, Math.min(90, req.neLat));
const lon1 = Math.max(-180, Math.min(180, req.swLon));
const lon2 = Math.max(-180, Math.min(180, req.neLon));
req = { ...req, icao24, callsign, swLat: Math.min(lat1, lat2), neLat: Math.max(lat1, lat2), swLon: Math.min(lon1, lon2), neLon: Math.max(lon1, lon2) };
if (icao24 || callsign) await admitIdentifierLookup(ctx.request);
const redistributableOnly = requiresRedistributableProviders(ctx.request);
const cacheKey = `${await buildCacheKey(req)}${redistributableOnly ? ':redistributable' : ''}`;
let result: { positions: PositionSample[]; source: string } | null = null;
try {
const positiveTtl = req.callsign ? CALLSIGN_CACHE_TTL : CACHE_TTL;
const negativeTtl = req.callsign ? CALLSIGN_NEGATIVE_TTL : CACHE_TTL;
result = await cachedFetchJson<{ positions: PositionSample[]; source: string }>(
cacheKey, positiveTtl, async () => {
const relayBase = getRelayBaseUrl();
const isCallsignOnly = !!req.callsign && !req.icao24 && isDegenerateBbox(req);View on GitHub (pinned to e586b8b4b8)