Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
13e57d4
fix: raise BadResponse when a response is not valid JSON
chipkent Oct 26, 2026
f309e9c
Merge #4 fix: log the topic for an invalid subscription
chipkent Oct 26, 2026
1a5bcba
Merge #7 feat: add funding-conditions and crypto daily open/close end…
chipkent Oct 26, 2026
3a83dac
Merge #8 fix: stop sending the options argument as a query parameter
chipkent Oct 26, 2026
fe32fbb
Merge #9 test: stop pook mocks leaking between REST test classes
chipkent Oct 26, 2026
0493791
Merge #10 ci: run unit tests on Python 3.12, 3.13 and 3.14
chipkent Oct 26, 2026
a363dac
Merge #11 ci: run unit tests on free-threaded Python 3.14t
chipkent Oct 26, 2026
3756564
Merge #12 fix: include the HTTP status in BadResponse
chipkent Oct 26, 2026
193922c
Merge #13 feat: add raise_on_status to RESTClient
chipkent Oct 26, 2026
a66f960
Merge #31 fix: raise BadResponse when a response is not valid JSON
chipkent Oct 26, 2026
499568f
Merge #14 fix: correct type hints that disagree with the API
chipkent Oct 26, 2026
aa81a03
Merge #15 feat: let type checkers see model constructors
chipkent Oct 26, 2026
0f72a64
Merge #16 feat: ship py.typed so the package is checked as typed
chipkent Oct 26, 2026
6015dc1
Merge #17 feat: overload REST methods on raw so callers get precise r…
chipkent Oct 26, 2026
ec52fce
Merge #18 fix: match WebSocket wildcard topics on the exact channel
chipkent Oct 26, 2026
9ed1f4e
Merge #19 fix: keep pending WebSocket subscriptions across a reconnect
chipkent Oct 26, 2026
23dc03b
Merge #20 fix: make WebSocket subscribe and unsubscribe thread-safe
chipkent Oct 26, 2026
37bcb13
Merge #21 feat: send WebSocket subscription changes without waiting f…
chipkent Oct 26, 2026
42b4ebb
Merge #22 feat: add WebSocketClient.set_subscriptions() and resubscri…
chipkent Oct 26, 2026
53d9b9d
Merge #23 feat: add opt-in WebSocket reconnect options
chipkent Oct 26, 2026
ded0303
Merge #24 feat: add WebSocketClient.request_close()
chipkent Oct 26, 2026
a16a55c
Rebuild deephaven-fixes as the merge of the submitted series (#4, #7-…
chipkent Oct 26, 2026
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 @@ -114,7 +114,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 @@ -160,11 +166,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 @@ -250,11 +252,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
Expand Up @@ -10,6 +10,7 @@
from massive.exceptions import BadResponse
from base import BaseTest

import json
import pook # type: ignore


Expand All @@ -33,6 +34,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