Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions posthog/request.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import math
import re
import socket
import time
Expand Down Expand Up @@ -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:
Comment thread
marandaneto marked this conversation as resolved.
return super().encode(_replace_non_finite_floats(o))
40 changes: 40 additions & 0 deletions posthog/test/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions references/public_api_snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading