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

  1. Inspect the violations array in the thrown ValidationError to see which field was non-finite.
  2. 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).
  3. Clamp or reject NaN/Infinity client-side; send zoom as a plain integer 0-22.
  4. 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

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


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)