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

  1. Validate all four coordinates are finite numbers before calling (Number.isFinite on each)
  2. Only invoke trackAircraft after the map viewport/bounds are initialized
  3. Coerce numeric strings with Number() and reject NaN prior to the request
  4. 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

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


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)