Skip to content

Commit df6e869

Browse files
giulio-leoneCopilot
andcommitted
fix: strengthen TogetherException repr handling
- preserve mapping-like headers as structured JSON in __repr__ - validate the regression with a real CIMultiDictProxy repro Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3e18160 commit df6e869

2 files changed

Lines changed: 29 additions & 32 deletions

File tree

src/together/error.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
from __future__ import annotations
22

33
import json
4+
from collections.abc import Mapping
45
from typing import Any, Dict
56

67
from requests import RequestException
78

89
from together.types.error import TogetherErrorResponse
910

1011

12+
def _json_safe_headers(headers: str | Dict[Any, Any] | None) -> str | Dict[Any, Any]:
13+
if headers is None:
14+
return {}
15+
if isinstance(headers, str):
16+
return headers
17+
if isinstance(headers, Mapping):
18+
return {str(key): value for key, value in headers.items()}
19+
return str(headers)
20+
21+
1122
class TogetherException(Exception):
1223
def __init__(
1324
self,
@@ -43,7 +54,7 @@ def __repr__(self) -> str:
4354
"response": self._message,
4455
"status": self.http_status,
4556
"request_id": self.request_id,
46-
"headers": self.headers,
57+
"headers": _json_safe_headers(self.headers),
4758
},
4859
default=str,
4960
)

tests/unit/test_error_repr.py

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,18 @@
33
from __future__ import annotations
44

55
import json
6-
from collections import OrderedDict
7-
from typing import Any, Iterator
86
from unittest.mock import MagicMock
97

10-
import pytest
8+
from multidict import CIMultiDict, CIMultiDictProxy
119

1210
from together.error import (
13-
TogetherException,
11+
APIError,
1412
AuthenticationError,
1513
ResponseError,
16-
APIError,
14+
TogetherException,
1715
)
1816

1917

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-
3918
class TestExceptionReprNonSerializable:
4019
"""Verify __repr__ doesn't crash on non-JSON-serializable headers (issue #108)."""
4120

@@ -52,21 +31,28 @@ def test_repr_with_dict_headers(self) -> None:
5231
assert "test error" in result
5332

5433
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"}
34+
"""Real CIMultiDictProxy headers must not crash repr (issue #108)."""
35+
headers = CIMultiDictProxy(
36+
CIMultiDict(
37+
{"Content-Type": "application/json", "X-Request-Id": "abc"}
38+
)
5839
)
5940
exc = TogetherException(
6041
message="server error",
61-
headers=fake_headers, # type: ignore[arg-type]
42+
headers=headers, # type: ignore[arg-type]
6243
http_status=500,
6344
request_id="req-456",
6445
)
65-
# Before fix: TypeError: Object of type FakeMultiDictProxy
46+
# Before fix: TypeError: Object of type CIMultiDictProxy
6647
# is not JSON serializable
6748
result = repr(exc)
6849
assert "TogetherException" in result
6950
assert "server error" in result
51+
parsed = json.loads(result[len("TogetherException(") + 1 : -2])
52+
assert parsed["headers"] == {
53+
"Content-Type": "application/json",
54+
"X-Request-Id": "abc",
55+
}
7056

7157
def test_repr_with_none_headers(self) -> None:
7258
"""None headers (default) should work."""
@@ -110,12 +96,12 @@ def test_repr_output_is_valid_after_fix(self) -> None:
11096

11197
def test_subclass_repr_with_non_serializable_headers(self) -> None:
11298
"""Subclasses should also benefit from the fix."""
113-
fake_headers = FakeMultiDictProxy({"X-Rate-Limit": "100"})
99+
headers = CIMultiDictProxy(CIMultiDict({"X-Rate-Limit": "100"}))
114100

115101
for ExcClass in (AuthenticationError, ResponseError, APIError):
116102
exc = ExcClass(
117103
message="subclass test",
118-
headers=fake_headers, # type: ignore[arg-type]
104+
headers=headers, # type: ignore[arg-type]
119105
http_status=429,
120106
)
121107
result = repr(exc)

0 commit comments

Comments
 (0)