Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## [3.8.3] - 2026-06-24

- [Fixed] `_sub_routing_loop` (and thus all websocket subscriptions) now tolerates empty `recv()` results from the WSS (zero-length frames), which previously produced `orjson.JSONDecodeError: Input is a zero-length, empty document` and an ERROR log that killed the router thread. Empty (and any other) `orjson` decode failures on the wire are now routed through `TRANSIENT_WS_ERRORS` so they log at WARNING ("WSS connection reset or closed by peer") and trigger the existing reconnect path. (OPS-3597)

## [3.8.2] - 2026-06-23

- [Fixed] `_new_conn` now closes the previous WSS connection before opening a new one, instead of overwriting `self._conn` and abandoning the old socket. On reconnect (when `_sub_routing_loop` sets `wss_conn_halted` and calls `_new_conn`) the orphaned socket lingered half-open on the server — no TCP FIN was ever sent — so the gateway kept fanning subscription events into a dead connection, accumulating an unbounded send buffer/mailbox until it OOMed. A new private `_close_conn` helper best-effort closes `self._conn` and clears the handle; it runs at the top of `_new_conn` so the server receives a FIN and tears down the stale subscription, and is reused by `close()` to drop the duplicated teardown.
Expand Down
1 change: 1 addition & 0 deletions pygqlc/GraphQLClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
BrokenPipeError,
ConnectionAbortedError,
websocket.WebSocketConnectionClosedException,
orjson.JSONDecodeError,
)

# * Custom Exception class for GraphQL responses
Expand Down
2 changes: 1 addition & 1 deletion pygqlc/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.8.2"
__version__ = "3.8.3"
25 changes: 25 additions & 0 deletions tests/pygqlc/gql_client/test_subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,28 @@ def make_conn(*_args, **_kwargs):
)
gql.closing = True
Singleton._instances.pop(GraphQLClient, None)


def test_sub_routing_loop_empty_recv_logged_as_warning(routing_client):
"""OPS-3597: empty recv() result (zero-length WSS frame -> orjson.loads(b'')) must
not crash the routing loop with JSONDecodeError; treat as transient close (like
ConnectionResetError), log at WARNING, set halted, and trigger reconnect path."""
gql = routing_client
gql._conn.recv.return_value = b""

with (
patch.object(gql, "_new_conn", side_effect=_stop_loop_on(gql)) as new_conn,
_capture_logs() as records,
):
_run_routing_loop(gql)

assert gql.wss_conn_halted is True
assert new_conn.called
assert any(
level == LogLevel.WARNING and "reset or closed by peer" in msg
for level, msg in records
)
assert not any(
level == LogLevel.ERROR and "Some error trying to receive WSS" in msg
for level, msg in records
)