From 86a1289184c3a16ee10844d9a74da1c8e297f3af Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Fri, 17 Apr 2026 21:44:35 +0500 Subject: [PATCH 1/2] refactor: drop json5 dep, handle non-finite floats in socket parse fallback Happy path uses native JSON.parse; the catch rewrites Python"s bare Infinity/-Infinity/NaN tokens outside string literals before retrying, so only payloads that actually contain specials pay the extra cost. NaN has no JSON literal and becomes null (matches JSON.stringify). --- .../reflex_base/.templates/web/utils/state.js | 25 ++++++++++++++++--- .../src/reflex_base/constants/installer.py | 1 - tests/integration/test_computed_vars.py | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index fd2b5e5741b..3a64921a284 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -1,6 +1,5 @@ // State management for Reflex web apps. import io from "socket.io-client"; -import JSON5 from "json5"; import env from "$/env.json"; import reflexEnvironment from "$/reflex.json"; import Cookies from "universal-cookie"; @@ -436,6 +435,22 @@ const resolveSocket = (socket) => { return socket?.current ?? socket; }; +// Python's json.dumps emits bare Infinity/-Infinity/NaN tokens (invalid JSON). +// Rewrite them outside string literals so JSON.parse accepts the payload. +// 1e999 / -1e999 overflow to ±Infinity; NaN has no JSON literal so it becomes null. +// The alternation matches whole string literals first (passed through unchanged), +// guaranteeing bare-token matches only land in numeric positions. +const NON_FINITE_FLOAT_RE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g; +const NON_FINITE_REPLACEMENTS = { + Infinity: "1e999", + "-Infinity": "-1e999", + NaN: "null", +}; +const rewriteBareNonFiniteFloats = (str) => + str.replace(NON_FINITE_FLOAT_RE, (match) => + match[0] === '"' ? match : NON_FINITE_REPLACEMENTS[match], + ); + /** * Queue events to be processed and trigger processing of queue. * @param events Array of events to queue. @@ -541,9 +556,13 @@ export const connect = async ( socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v); socket.current.io.decoder.tryParse = (str) => { try { - return JSON5.parse(str); + return JSON.parse(str); } catch (e) { - return false; + try { + return JSON.parse(rewriteBareNonFiniteFloats(str)); + } catch (e2) { + return false; + } } }; // Set up a reconnect helper function diff --git a/packages/reflex-base/src/reflex_base/constants/installer.py b/packages/reflex-base/src/reflex_base/constants/installer.py index 1578ace7f2c..d4891d8ae2d 100644 --- a/packages/reflex-base/src/reflex_base/constants/installer.py +++ b/packages/reflex-base/src/reflex_base/constants/installer.py @@ -122,7 +122,6 @@ def DEPENDENCIES(cls) -> dict[str, str]: A dictionary of dependencies with their versions. """ return { - "json5": "2.2.3", "react-router": cls._react_router_version, "react-router-dom": cls._react_router_version, "@react-router/node": cls._react_router_version, diff --git a/tests/integration/test_computed_vars.py b/tests/integration/test_computed_vars.py index 905b1594cb5..744ec1ad296 100644 --- a/tests/integration/test_computed_vars.py +++ b/tests/integration/test_computed_vars.py @@ -232,7 +232,7 @@ async def test_computed_vars( special_floats = driver.find_element(By.ID, "special_floats") assert special_floats - assert special_floats.text == "42.9, NaN, Infinity, -Infinity" + assert special_floats.text == "42.9, , Infinity, -Infinity" increment = driver.find_element(By.ID, "increment") assert increment.is_enabled() From 24b214cefb9b6a56566d604b73ecaa04808f2cb6 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Sat, 18 Apr 2026 01:00:52 +0500 Subject: [PATCH 2/2] fix: preserve NaN through socket parse fallback instead of coercing to null Swap bare NaN for a sentinel string before JSON.parse and revive it back to a real NaN so Python-side float(nan) round-trips to the frontend. --- .../src/reflex_base/.templates/web/utils/state.js | 14 ++++++++++---- tests/integration/test_computed_vars.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index 3a64921a284..d931a19299d 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -437,19 +437,22 @@ const resolveSocket = (socket) => { // Python's json.dumps emits bare Infinity/-Infinity/NaN tokens (invalid JSON). // Rewrite them outside string literals so JSON.parse accepts the payload. -// 1e999 / -1e999 overflow to ±Infinity; NaN has no JSON literal so it becomes null. +// 1e999 / -1e999 overflow to ±Infinity; NaN has no JSON literal, so it is +// swapped for a sentinel string and revived back to NaN after parsing. // The alternation matches whole string literals first (passed through unchanged), // guaranteeing bare-token matches only land in numeric positions. -const NON_FINITE_FLOAT_RE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g; +const NAN_SENTINEL = "__reflex_nan__"; +const NON_FINITE_FLOAT_RE = /"(?:[^"\\]|\\.)*"|-?\bInfinity\b|\bNaN\b/g; const NON_FINITE_REPLACEMENTS = { Infinity: "1e999", "-Infinity": "-1e999", - NaN: "null", + NaN: `"${NAN_SENTINEL}"`, }; const rewriteBareNonFiniteFloats = (str) => str.replace(NON_FINITE_FLOAT_RE, (match) => match[0] === '"' ? match : NON_FINITE_REPLACEMENTS[match], ); +const reviveNonFiniteFloats = (_k, v) => (v === NAN_SENTINEL ? NaN : v); /** * Queue events to be processed and trigger processing of queue. @@ -559,7 +562,10 @@ export const connect = async ( return JSON.parse(str); } catch (e) { try { - return JSON.parse(rewriteBareNonFiniteFloats(str)); + return JSON.parse( + rewriteBareNonFiniteFloats(str), + reviveNonFiniteFloats, + ); } catch (e2) { return false; } diff --git a/tests/integration/test_computed_vars.py b/tests/integration/test_computed_vars.py index 744ec1ad296..905b1594cb5 100644 --- a/tests/integration/test_computed_vars.py +++ b/tests/integration/test_computed_vars.py @@ -232,7 +232,7 @@ async def test_computed_vars( special_floats = driver.find_element(By.ID, "special_floats") assert special_floats - assert special_floats.text == "42.9, , Infinity, -Infinity" + assert special_floats.text == "42.9, NaN, Infinity, -Infinity" increment = driver.find_element(By.ID, "increment") assert increment.is_enabled()