{"record":{"id":"b91607f19afe0d12","repo":"koala73/worldmonitor","slug":"must-be-a-finite-number","errorCode":null,"errorMessage":"Must be a finite number","messagePattern":"Must be a finite number","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"warning","filePath":"server/worldmonitor/webcam/v1/list-webcams.ts","lineNumber":85,"sourceCode":"      });\n    }\n  }\n\n  return { singles, clusters };\n}\n\nexport async function listWebcams(_ctx: ServerContext, req: ListWebcamsRequest): Promise<ListWebcamsResponse> {\n  const values = {\n    zoom: req.zoom ?? 3,\n    boundW: req.boundW ?? -180,\n    boundS: req.boundS ?? -90,\n    boundE: req.boundE ?? 180,\n    boundN: req.boundN ?? 90,\n  };\n  const violations = Object.entries(values)\n    .filter(([, value]) => !Number.isFinite(value))\n    .map(([field]) => ({ field, description: 'Must be a finite number' }));\n  if (violations.length) throw new ValidationError(violations);\n\n  // MapLibre supports zoom through 22. Preserve the existing <3 / <=4 / <=6\n  // / <=8 clustering boundaries while collapsing fractional cache identities.\n  const zoom = Math.max(0, Math.min(22,\n    values.zoom < 3 ? Math.floor(values.zoom) : Math.ceil(values.zoom)));\n\n  // Clamp before quantization: the global map still needs the full 360 x 180\n  // degree box, but no query can exceed that globe-sized maximum. Every\n  // viewport sharing a quantized key must use the same superset query.\n  const qW = Math.floor(Math.max(-180, Math.min(180, values.boundW)));\n  const qS = Math.floor(Math.max(-90, Math.min(90, values.boundS)));\n  const qE = Math.ceil(Math.max(-180, Math.min(180, values.boundE)));\n  const qN = Math.ceil(Math.max(-90, Math.min(90, values.boundN)));\n\n  // Read active version\n  const versionResult = await getCachedJson('webcam:cameras:active', true);\n  const version = versionResult != null ? String(versionResult) : null;\n  if (!version) {","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/webcam/v1/list-webcams.ts#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst params = new URLSearchParams({ boundW: String(state.west), zoom: state.zoomText });\n// after\nconst boundW = Number(state.west);\nconst zoom = Math.floor(Number(state.zoomText));\nif (!Number.isFinite(boundW) || !Number.isFinite(zoom)) throw new TypeError('bounds/zoom must be finite numbers');\nconst params = new URLSearchParams({ boundW: String(boundW), zoom: String(zoom) });","handlingStrategy":"validation","validationCode":"const values = { boundW: -180, boundE: 180, boundS: -90, boundN: 90, zoom: 4, ...provided };\nconst bad = Object.entries(values).filter(([, v]) => !Number.isFinite(v));\nif (bad.length) throw new TypeError(`Non-finite params: ${bad.map(([k]) => k).join(', ')}`);","typeGuard":"const isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);","tryCatchPattern":"try {\n  const data = await listWebcams(req);\n} catch (e) {\n  if (e instanceof ValidationError && e.violations?.some(v => v.description === 'Must be a finite number')) {\n    console.warn('Fix numeric params:', e.violations.map(v => v.field));\n    return;\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["validation","api","request-parameters","finite-number"],"backgroundTag":"invalid-argument-value","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}