Skip to content

Commit 6cd9a96

Browse files
giulio-leoneCopilot
andcommitted
fix: prevent TogetherException repr crash on non-JSON-serializable headers
When headers contain non-JSON-serializable objects (e.g. aiohttp's CIMultiDictProxy), `json.dumps()` in `__repr__` raises TypeError, making it impossible to print or log the exception. Add `default=str` to json.dumps so non-serializable objects fall back to their string representation instead of crashing. Fixes #108 Signed-off-by: Giulio Leone <6887247+giulio-leone@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 35fd835 commit 6cd9a96

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

src/together/error.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def __repr__(self) -> str:
4444
"status": self.http_status,
4545
"request_id": self.request_id,
4646
"headers": self.headers,
47-
}
47+
},
48+
default=str,
4849
)
4950
return "%s(%r)" % (self.__class__.__name__, repr_message)
5051

tests/unit/test_error_repr.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Tests for TogetherException.__repr__ with non-JSON-serializable headers."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from collections import OrderedDict
7+
from typing import Any, Iterator
8+
from unittest.mock import MagicMock
9+
10+
import pytest
11+
12+
from together.error import (
13+
TogetherException,
14+
AuthenticationError,
15+
ResponseError,
16+
APIError,
17+
)
18+
19+
20+
class FakeMultiDictProxy:
21+
"""Simulates aiohttp's CIMultiDictProxy — not JSON serializable."""
22+
23+
def __init__(self, data: dict[str, str]) -> None:
24+
self._data = data
25+
26+
def __iter__(self) -> Iterator[str]:
27+
return iter(self._data)
28+
29+
def __len__(self) -> int:
30+
return len(self._data)
31+
32+
def __getitem__(self, key: str) -> str:
33+
return self._data[key]
34+
35+
def __repr__(self) -> str:
36+
return f"<FakeMultiDictProxy({self._data!r})>"
37+
38+
39+
class TestExceptionReprNonSerializable:
40+
"""Verify __repr__ doesn't crash on non-JSON-serializable headers (issue #108)."""
41+
42+
def test_repr_with_dict_headers(self) -> None:
43+
"""Normal dict headers should still work fine."""
44+
exc = TogetherException(
45+
message="test error",
46+
headers={"Content-Type": "application/json"},
47+
http_status=400,
48+
request_id="req-123",
49+
)
50+
result = repr(exc)
51+
assert "TogetherException" in result
52+
assert "test error" in result
53+
54+
def test_repr_with_multidict_proxy_headers(self) -> None:
55+
"""CIMultiDictProxy-like headers must not crash repr (issue #108)."""
56+
fake_headers = FakeMultiDictProxy(
57+
{"Content-Type": "application/json", "X-Request-Id": "abc"}
58+
)
59+
exc = TogetherException(
60+
message="server error",
61+
headers=fake_headers, # type: ignore[arg-type]
62+
http_status=500,
63+
request_id="req-456",
64+
)
65+
# Before fix: TypeError: Object of type FakeMultiDictProxy
66+
# is not JSON serializable
67+
result = repr(exc)
68+
assert "TogetherException" in result
69+
assert "server error" in result
70+
71+
def test_repr_with_none_headers(self) -> None:
72+
"""None headers (default) should work."""
73+
exc = TogetherException(message="no headers")
74+
result = repr(exc)
75+
assert "TogetherException" in result
76+
77+
def test_repr_with_string_headers(self) -> None:
78+
"""String headers should work."""
79+
exc = TogetherException(
80+
message="string headers", headers="raw-header-string"
81+
)
82+
result = repr(exc)
83+
assert "TogetherException" in result
84+
85+
def test_repr_with_nested_non_serializable(self) -> None:
86+
"""Dict headers containing non-serializable values should not crash."""
87+
exc = TogetherException(
88+
message="nested issue",
89+
headers={"key": MagicMock()}, # type: ignore[dict-item]
90+
http_status=502,
91+
)
92+
result = repr(exc)
93+
assert "TogetherException" in result
94+
95+
def test_repr_output_is_valid_after_fix(self) -> None:
96+
"""repr should produce parseable output (class name + JSON string)."""
97+
exc = TogetherException(
98+
message="validation error",
99+
headers={"Authorization": "Bearer ***"},
100+
http_status=422,
101+
request_id="req-789",
102+
)
103+
result = repr(exc)
104+
assert result.startswith("TogetherException(")
105+
# The inner string should be valid JSON
106+
inner = result[len("TogetherException(") + 1 : -2] # strip quotes
107+
parsed = json.loads(inner)
108+
assert parsed["status"] == 422
109+
assert parsed["request_id"] == "req-789"
110+
111+
def test_subclass_repr_with_non_serializable_headers(self) -> None:
112+
"""Subclasses should also benefit from the fix."""
113+
fake_headers = FakeMultiDictProxy({"X-Rate-Limit": "100"})
114+
115+
for ExcClass in (AuthenticationError, ResponseError, APIError):
116+
exc = ExcClass(
117+
message="subclass test",
118+
headers=fake_headers, # type: ignore[arg-type]
119+
http_status=429,
120+
)
121+
result = repr(exc)
122+
assert ExcClass.__name__ in result

0 commit comments

Comments
 (0)