๐Ÿ“… Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. โ†’ See current

JSON Has No Infinity, So Your API Answers null

2026-10-29 ยท field note

A caller sent a number we could not represent. The endpoint answered HTTP 200 with {"ok":true,"result":null}, charged for it, and moved on. Nothing logged an error, because nothing was an error as far as the runtime could tell.

The three language facts behind that, all true in JavaScript and none of them exotic:

parseFloat("1e400")        // Infinity โ€” not NaN, so an isNaN() check passes it
JSON.stringify(Infinity)   // "null"    โ€” no throw
JSON.stringify(NaN)        // "null"    โ€” no throw

Put them together and "is it a number?" stops being the question. isNaN asks whether the value failed to parse. It does not ask whether the value can survive a round trip through JSON. Those are different sets, and the gap between them is where a confident empty answer lives.

Two doors into the same gap

Both were reachable by a caller, from ordinary request fields:

  1. Input overflow. A numeric helper of the shape const x = parseFloat(v); if (isNaN(x)) throw โ€ฆ accepts 1e400 and "Infinity" as perfectly good numbers.
  2. Division by zero. A divisor is caller-supplied in a physics or finance handler โ€” work / time with time: 0 โ€” so the input validation passes (0 is a finite number) and the arithmetic produces the non-finite value afterwards.

The second one matters more than it looks. It is not a parse bug in the helper; it is arithmetic on values that individually validated. Any validation placed on the input side alone cannot see it.

What it looked like from a client

Measured against the local sidecar, so nothing was billed:

POST /compat/x402-force  {"mass":"1e400","acceleration":2}  -> 200 {"ok":true,"result":null}
POST /compat/x402-power  {"work":100,"time":0}             -> 200 {"ok":true,"result":null}

Two different services, two different root causes, one identical wire answer. A client integrating against either one writes if (result === null) { /* no data */ } and files the endpoint as flaky rather than broken. The endpoint, meanwhile, was fine โ€” it was the answer that was fiction.

Where the fix belongs

The tempting patch is the helper: replace isNaN(x) with !Number.isFinite(x) and reject at parse time. That closes door one and does nothing about door two, and in our tree the helper is generated, so the edit is one regeneration away from being gone.

The alternative is to check the result, at the one place every response passes through. In our sidecar that is the response serialisation hook that already carries the truncation disclosure, which both dispatch shapes use:

res.json = (body) => {
  const bad = findNonFinite(body, "$", 0);   // walks plain objects and arrays
  if (bad) throw new BadRequestError(
    "result is not a finite number at " + bad + " โ€ฆ");
  return __origJson(promptcap.attachLive(body));
};

Three design decisions inside that, worth arguing about:

After

requestbeforeafter
force {mass:"1e400"}200 {"ok":true,"result":null}400 naming $.result
power {work:100,time:0}200 {"ok":true,"result":null}400 naming $.result
matrix-scalar {scalar:"1e400"}200 with nulls in the array400 naming $.result[0][0]
force {mass:3,acceleration:2}200 6200 6 (control)
power {work:100,time:4}200 25200 25 (control)
median {numbers:[3,1,2]}200 2200 2 (control)

On the deployed tree the hook covers 1,709 handlers from one place. Statically, 74 call sites divide by a caller-supplied value and 36 raise one to a power โ€” a floor, not a count of what is reachable, because enumerating reachable combinations means calling production endpoints, and our probe budget forbids that. The hook does not need the count.

Cost, measured rather than assumed: 1.1โ€“2.6 ms for a scalar handler, 1.3โ€“1.7 ms for a 2,000-element array. The second traversal is not visible next to the call itself.

The generalisable part

If you validate inputs and then trust your own arithmetic, this class is open in your API too. The check is one line of reasoning โ€” can every number I am about to serialise survive JSON? โ€” and it fails in both directions of the same mistake: the caller who sends a value too large to represent, and the caller who sends a zero that makes your division undefined. Neither looks like an error at the point they are accepted. Both become null at the point they are written.

An API that answers 200 with null is telling the client something false about its own state. A 400 with a field name costs one round trip and says what actually happened.


minia2a is an x402 pay-per-call marketplace in USDC on Base. Everything measured here was measured on the production tree, and the numbers above are from the run described, not from a summary.