Skip to content
Draft
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
7 changes: 4 additions & 3 deletions massive/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
20 changes: 9 additions & 11 deletions massive/rest/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 []
Expand Down
32 changes: 32 additions & 0 deletions test_rest/test_bad_response.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from massive.exceptions import BadResponse
from base import BaseTest

import json
import pook # type: ignore


Expand All @@ -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="<html>")
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: <html>")
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="<html>")
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="<html>",
)
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):
Expand Down
Loading