From f29b17324dd157826dd5ad2c243d25317828a8f5 Mon Sep 17 00:00:00 2001 From: Harsh Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:28:12 -0500 Subject: [PATCH 1/3] fix: serialize non-finite floats as null in JSON payloads Added a helper function to replace non-finite floats with None in JSON serialization. --- posthog/request.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/posthog/request.py b/posthog/request.py index 76df1fdc9..1c2456a77 100644 --- a/posthog/request.py +++ b/posthog/request.py @@ -1,5 +1,6 @@ import json import logging +import math import re import socket import time @@ -479,9 +480,29 @@ class QuotaLimitError(APIError): RequestsConnectionError = requests.exceptions.ConnectionError +def _replace_non_finite_floats(value: Any) -> Any: + """Recursively replace inf/nan floats with None. + + json.dumps with the default allow_nan=True emits bare Infinity/NaN tokens, + which are not valid JSON and get rejected by strict-parsing ingestion (and + intermediaries). posthog-js avoids this because JSON.stringify converts + non-finite numbers to null; this mirrors that behavior. + """ + if isinstance(value, float): + return value if math.isfinite(value) else None + if isinstance(value, dict): + return {key: _replace_non_finite_floats(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_replace_non_finite_floats(item) for item in value] + return value + + class DatetimeSerializer(json.JSONEncoder): def default(self, obj: Any): if isinstance(obj, (date, datetime)): return obj.isoformat() return json.JSONEncoder.default(self, obj) + + def encode(self, o: Any) -> str: + return super().encode(_replace_non_finite_floats(o)) From 06a437a350846b46362a14161d4f2996154f184e Mon Sep 17 00:00:00 2001 From: Harsh Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:29:05 -0500 Subject: [PATCH 2/3] test: cover non-finite float serialization in batch payloads --- posthog/test/test_request.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/posthog/test/test_request.py b/posthog/test/test_request.py index d4ad7c9da..455f24d82 100644 --- a/posthog/test/test_request.py +++ b/posthog/test/test_request.py @@ -776,3 +776,43 @@ def test_flags_does_not_retry_other_http_status_errors(self, status_code): self.assertEqual(cm.exception.status, status_code) self.assertEqual(mock_session.post.call_count, 1) + + +def test_post_serializes_non_finite_floats_as_null(): + mock_response = requests.Response() + mock_response.status_code = 200 + mock_session = mock.MagicMock() + mock_session.post.return_value = mock_response + + request_module.post( + TEST_API_KEY, + host="https://test.posthog.com", + path="/batch/", + session=mock_session, + batch=[ + { + "event": "score_computed", + "distinct_id": "user-1", + "properties": { + "score": float("inf"), + "nested": [float("nan"), {"depth": float("-inf")}], + "ok": 1.5, + }, + } + ], + ) + + raw = mock_session.post.call_args.kwargs["data"] + # Bare Infinity/NaN tokens are not valid JSON; a strict parser must accept the body. + data = json.loads( + raw, parse_constant=lambda x: (_ for _ in ()).throw(ValueError(x)) + ) + props = data["batch"][0]["properties"] + assert props["score"] is None + assert props["nested"] == [None, {"depth": None}] + assert props["ok"] == 1.5 + + +def test_datetime_serializer_keeps_finite_values(): + body = {"n": 1.5, "s": "x", "b": True, "i": 3, "none": None} + assert json.loads(json.dumps(body, cls=DatetimeSerializer)) == body From 9cb99ffd98ba6e975ede148605544687599e9405 Mon Sep 17 00:00:00 2001 From: Harsh <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:59:42 +0530 Subject: [PATCH 3/3] chore: refresh public API snapshot for DatetimeSerializer.encode --- references/public_api_snapshot.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 29a1c477e..645f6c42b 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -1718,6 +1718,7 @@ method posthog.metrics_capture.PostHogMetrics.reset() -> None method posthog.poller.Poller.run() method posthog.poller.Poller.stop() method posthog.request.DatetimeSerializer.default(obj: Any) +method posthog.request.DatetimeSerializer.encode(o: Any) -> str method posthog.request.HTTPAdapterWithSocketOptions.init_poolmanager(*args, **kwargs) method posthog.tracing.span.Span.add_event(name: str, attributes: Optional[Mapping[str, Any]] = None, timestamp: Union[datetime, float, None] = None) -> Span method posthog.tracing.span.Span.end(end_time: Union[datetime, float, None] = None) -> None