Skip to content

Commit 680c9f1

Browse files
fix: narrow _run_protocol invalidation to transport-level errors
The catch-all except BaseException invalidated the connection on any error — including client-side encoding bugs, user TypeError/ValueError from bad params, and other pre-write failures that leave the wire state intact. Every such error destroyed the socket and, via the pool's drain-on-dead-connection logic, tore down all idle connections too. Invalidate only for the actual wire-state-damaging cases: DqliteConnectionError, ProtocolError, OperationalError with a leader code, and CancelledError / KeyboardInterrupt / SystemExit (where the request/response round-trip was interrupted mid-flight). Let programming / encoding errors propagate with the connection intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bf8ddf3 commit 680c9f1

2 files changed

Lines changed: 24 additions & 1 deletion

File tree

src/dqliteclient/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ async def _run_protocol[T](self, fn: Callable[[DqliteProtocol, int], Awaitable[T
241241
if e.code in _LEADER_ERROR_CODES:
242242
self._invalidate()
243243
raise
244-
except BaseException:
244+
except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
245+
# Interrupted mid-operation; we don't know how much of the
246+
# request/response round-trip completed, so the wire state is
247+
# unsafe to reuse. Invalidate and re-raise.
245248
self._invalidate()
246249
raise
247250
finally:

tests/test_connection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,26 @@ async def test_connect_open_not_leader_surfaces_as_connection_error(self) -> Non
758758
# Socket must also be cleaned up.
759759
assert not conn.is_connected
760760

761+
async def test_client_side_error_does_not_invalidate_connection(
762+
self, connected_connection
763+
) -> None:
764+
"""A client-side error (e.g., encoding bug) before any wire byte is
765+
written must not destroy the connection. Only transport-level and
766+
leader-change errors warrant invalidation.
767+
"""
768+
conn, _, _ = connected_connection
769+
assert conn.is_connected
770+
771+
async def raise_client_error(_db, _sql, _params=None):
772+
raise TypeError("bad parameter type")
773+
774+
conn._protocol.exec_sql = raise_client_error # type: ignore[assignment]
775+
776+
with pytest.raises(TypeError, match="bad parameter"):
777+
await conn.execute("INSERT INTO t VALUES (?)", [object()])
778+
779+
assert conn.is_connected, "client-side TypeError must not invalidate the connection"
780+
761781
async def test_cross_event_loop_raises_interface_error(self) -> None:
762782
"""Using a connection from a different event loop must raise InterfaceError."""
763783
import asyncio

0 commit comments

Comments
 (0)