fix(examples/monitor-web): emit strict JSON by mapping NaN/Inf to null

`json.dumps` defaults to `allow_nan=True` and emits bare `NaN`/`Infinity`
tokens, which the browser's `JSON.parse` rejects with
``Unexpected token 'N'``. The collector writes `math.nan` for any metric
key that was sampled previously but is missing from the current snapshot,
so the dashboard hit this on its first poll.

Add a small `_finite()` walker that maps non-finite floats to `None`
(serialized as JSON `null`) before encoding, and switch the dumper to
`allow_nan=False` so any future leak fails loudly. The dashboard JS
already treats non-finite values as `'—'` via `Number.isFinite()`, so
`null` flows through to the same fallback with no client-side change.
This commit is contained in:
Xuehai Pan 2026-05-20 14:13:05 +08:00
parent 7b53aafdd4
commit 5c66a9ae28

View file

@ -27,6 +27,7 @@ from __future__ import annotations
import argparse
import http.server
import json
import math
import os
import re
import ssl
@ -257,7 +258,10 @@ class MonitorRequestHandler(http.server.BaseHTTPRequestHandler):
self._send_json(payload)
def _send_json(self, payload: object) -> None:
body = json.dumps(payload, default=float).encode('utf-8')
# `allow_nan=False` makes strict JSON; ``_finite()`` first maps
# `math.nan`/`math.inf` (which the collector emits for missing samples)
# to `None` so the browser's `JSON.parse` accepts the body.
body = json.dumps(_finite(payload), allow_nan=False, default=float).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Cache-Control', 'no-store')
@ -274,6 +278,22 @@ class MonitorRequestHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(body)
def _finite(value: Any) -> Any:
"""Replace `nan`/`+inf`/`-inf` with :data:`None` so the result is strict JSON.
The collector writes :data:`math.nan` for any metric key that was sampled previously but is
missing from the current snapshot (see :class:`nvitop.ResourceMetricCollector`), and strict
JSON has no representation for ``NaN`` or ``Infinity``.
"""
if isinstance(value, float):
return value if math.isfinite(value) else None
if isinstance(value, dict):
return {k: _finite(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_finite(v) for v in value]
return value
def _maybe_positive_int(text: str | None) -> int | None:
if text is None:
return None