koala73/worldmonitor · warning · ValidationError
Must be a finite number
Error message
Must be a finite number
What it means
The webcam list endpoint validates its bounding-box and zoom request parameters with Number.isFinite before using them. If any of boundW/boundE/boundS/boundN/zoom is missing in a way that yields a non-number (e.g. NaN after coercion), a ValidationError is thrown listing each offending field with the description 'Must be a finite number'. Defaults of 180/-180/90/-90 only apply when the property is undefined/null, not when an invalid value like 'abc' or NaN is supplied.
Solutions
- Inspect the violations array in the thrown ValidationError to see which field was non-finite.
- Parse query parameters with Number(value) and check Number.isFinite before sending; omit the field entirely to get the default (boundW=-180, boundE=180, boundS=-90, boundN=90).
- Clamp or reject NaN/Infinity client-side; send zoom as a plain integer 0-22.
- Update any client that sends stringly-typed numerics (e.g. from URLSearchParams) to convert them first.
Example fix
// before
const params = new URLSearchParams({ boundW: String(state.west), zoom: state.zoomText });
// after
const boundW = Number(state.west);
const zoom = Math.floor(Number(state.zoomText));
if (!Number.isFinite(boundW) || !Number.isFinite(zoom)) throw new TypeError('bounds/zoom must be finite numbers');
const params = new URLSearchParams({ boundW: String(boundW), zoom: String(zoom) }); Defensive patterns
Strategy: validation
Validate before calling
const values = { boundW: -180, boundE: 180, boundS: -90, boundN: 90, zoom: 4, ...provided };
const bad = Object.entries(values).filter(([, v]) => !Number.isFinite(v));
if (bad.length) throw new TypeError(`Non-finite params: ${bad.map(([k]) => k).join(', ')}`); Type guard
const isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
Try / catch
try {
const data = await listWebcams(req);
} catch (e) {
if (e instanceof ValidationError && e.violations?.some(v => v.description === 'Must be a finite number')) {
console.warn('Fix numeric params:', e.violations.map(v => v.field));
return;
}
throw e;
} Prevention
- Always Number() URL query params and Number.isFinite-check before sending.
- Omit optional bounds/zoom rather than sending empty or non-numeric strings.
- Share one validated param-builder between clients of this endpoint.
When it happens
Trigger: Calling the list-webcams API with a query parameter that parses to NaN (e.g. boundW=abc, zoom=), or explicitly passing NaN/Infinity/null-coerced values in the request object. Defaults only cover absent fields, so a present-but-invalid value triggers the violation.
Common situations: Client code building the query string from form inputs without parsing numbers; URL query params read as raw strings in the handler; a producer sending JSON with "zoom": "low"; stale clients after the endpoint started validating numerics.
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
- Invalid scorecard bloc selection.
- Expected an array of Sentry issues. A Sentry error body is a
- Sentry issues response was not an array
- Redis transaction failed: ${results?.error || 'invalid respo
- Invalid region: only global is supported
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/b91607f19afe0d12.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/webcam/v1/list-webcams.ts:85
});
}
}
return { singles, clusters };
}
export async function listWebcams(_ctx: ServerContext, req: ListWebcamsRequest): Promise<ListWebcamsResponse> {
const values = {
zoom: req.zoom ?? 3,
boundW: req.boundW ?? -180,
boundS: req.boundS ?? -90,
boundE: req.boundE ?? 180,
boundN: req.boundN ?? 90,
};
const violations = Object.entries(values)
.filter(([, value]) => !Number.isFinite(value))
.map(([field]) => ({ field, description: 'Must be a finite number' }));
if (violations.length) throw new ValidationError(violations);
// MapLibre supports zoom through 22. Preserve the existing <3 / <=4 / <=6
// / <=8 clustering boundaries while collapsing fractional cache identities.
const zoom = Math.max(0, Math.min(22,
values.zoom < 3 ? Math.floor(values.zoom) : Math.ceil(values.zoom)));
// Clamp before quantization: the global map still needs the full 360 x 180
// degree box, but no query can exceed that globe-sized maximum. Every
// viewport sharing a quantized key must use the same superset query.
const qW = Math.floor(Math.max(-180, Math.min(180, values.boundW)));
const qS = Math.floor(Math.max(-90, Math.min(90, values.boundS)));
const qE = Math.ceil(Math.max(-180, Math.min(180, values.boundE)));
const qN = Math.ceil(Math.max(-90, Math.min(90, values.boundN)));
// Read active version
const versionResult = await getCachedJson('webcam:cameras:active', true);
const version = versionResult != null ? String(versionResult) : null;
if (!version) {View on GitHub (pinned to 7d06c8633d)