diff --git a/massive/exceptions.py b/massive/exceptions.py index d5b1c40b..bb5b9186 100644 --- a/massive/exceptions.py +++ b/massive/exceptions.py @@ -14,10 +14,11 @@ class AuthError(Exception): class BadResponse(Exception): """ - Non-200 response from API + Non-200 response from API, or a 200 response whose body is not valid JSON - The message is the response body. ``status`` is the HTTP status code, or - None if not given. + The message is the response body; for invalid JSON it is "Invalid JSON in + response: " followed by the first 200 bytes of the body. ``status`` is the + HTTP status code, or None if not given. """ def __init__(self, *args: object, status: Optional[int] = None): diff --git a/massive/rest/base.py b/massive/rest/base.py index 7b728685..75591f07 100644 --- a/massive/rest/base.py +++ b/massive/rest/base.py @@ -111,7 +111,13 @@ def __init__( self.json = json def _decode(self, resp): - return self.json.loads(resp.data.decode("utf-8")) + try: + return self.json.loads(resp.data.decode("utf-8")) + except ValueError as e: + body = resp.data[:200].decode("utf-8", "replace") + raise BadResponse( + f"Invalid JSON in response: {body}", status=resp.status + ) from e def _get( self, @@ -157,11 +163,7 @@ def _get( if raw: return resp - try: - obj = self._decode(resp) - except ValueError as e: - logger.error("Error decoding json response: %s", e) - return [] + obj = self._decode(resp) if result_key: if result_key not in obj: @@ -246,11 +248,7 @@ def _paginate_iter( options=options, ) - try: - decoded = self._decode(resp) - except ValueError as e: - logger.error("Error decoding json response: %s", e) - return [] + decoded = self._decode(resp) if result_key not in decoded: return [] diff --git a/test_rest/test_bad_response.py b/test_rest/test_bad_response.py index 23c86e7b..e9282a13 100644 --- a/test_rest/test_bad_response.py +++ b/test_rest/test_bad_response.py @@ -1,6 +1,7 @@ from massive.exceptions import BadResponse from base import BaseTest +import json import pook # type: ignore @@ -24,6 +25,37 @@ def test_status_is_set(self): self.assertEqual(ctx.exception.args, (body,)) self.assertEqual(str(ctx.exception), body) + def test_invalid_json_raises(self): + pook.get(self.c.BASE + "/v3/reference/tickers/BADJSON", response_body="") + with self.assertRaises(BadResponse) as ctx: + self.c.get_ticker_details("BADJSON") + self.assertEqual(ctx.exception.status, 200) + self.assertEqual(str(ctx.exception), "Invalid JSON in response: ") + self.assertIsInstance(ctx.exception.__cause__, json.JSONDecodeError) + + def test_invalid_json_first_page_raises(self): + pook.get(self.c.BASE + "/v3/trades/BADJSON", response_body="") + with self.assertRaises(BadResponse) as ctx: + list(self.c.list_trades("BADJSON")) + self.assertEqual(ctx.exception.status, 200) + + def test_invalid_json_later_page_raises(self): + first = { + "results": [{"price": 1.0}], + "next_url": self.c.BASE + "/v3/trades/BADJSON2?cursor=2", + } + pook.get( + self.c.BASE + "/v3/trades/BADJSON2?cursor=2", + response_body="", + ) + pook.get(self.c.BASE + "/v3/trades/BADJSON2", response_json=first) + prices = [] + with self.assertRaises(BadResponse) as ctx: + for t in self.c.list_trades("BADJSON2"): + prices.append(t.price) + self.assertEqual(prices, [1.0]) + self.assertEqual(ctx.exception.status, 200) + def test_status_defaults_to_none(self): for args in [(), ("boom",), ("boom", 1)]: with self.subTest(args=args):