From 3a6f9fdbc4f599b974a5a7fba6b631c977f745a1 Mon Sep 17 00:00:00 2001 From: m1kados <75936762+m1kados@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:55:57 +0100 Subject: [PATCH] Fix: Raise GvmError when the remote closed the connection on send Sending a command over a connection that has been closed by the remote server, for example by gvmd after an error response, raised a raw BrokenPipeError/ConnectionResetError from the socket. Reading from a closed connection already results in a GvmError. Convert connection errors raised while sending or reading into a GvmError too, so callers can handle a closed connection consistently and re-connect. The original exception is kept as the cause. Closes #11 Co-Authored-By: Claude Fable 5.2 --- gvm/connections/_connection.py | 21 +++++++++-- tests/connections/test_gvm_connection.py | 44 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/gvm/connections/_connection.py b/gvm/connections/_connection.py index e4b3bba91..009fd8e21 100644 --- a/gvm/connections/_connection.py +++ b/gvm/connections/_connection.py @@ -82,11 +82,22 @@ def send(self, data: bytes) -> None: Args: data: Data to be send to the server as bytes. + + Raises: + GvmError: If the socket is not connected or the remote server + has closed the connection. In the latter case the connection + needs to be re-established via :py:meth:`connect`. """ if self._socket is None: raise GvmError("Socket is not connected") - self._socket.sendall(data) + try: + self._socket.sendall(data) + except ConnectionError as e: + # Connection was closed by server, e.g. after an error response + raise GvmError( + f"Remote closed the connection. Error was {e}" + ) from e def read(self) -> bytes: """Read data from the remote server @@ -98,7 +109,13 @@ def read(self) -> bytes: time() + self._timeout if self._timeout is not None else None ) - data = self._read() + try: + data = self._read() + except ConnectionError as e: + # Connection was reset by server + raise GvmError( + f"Remote closed the connection. Error was {e}" + ) from e if not data: # Connection was closed by server diff --git a/tests/connections/test_gvm_connection.py b/tests/connections/test_gvm_connection.py index b3aecfe9d..92b1e67a4 100644 --- a/tests/connections/test_gvm_connection.py +++ b/tests/connections/test_gvm_connection.py @@ -43,6 +43,50 @@ def test_read_no_data(self): with self.assertRaises(GvmError, msg="Remote closed the connection"): connection.read() + def test_read_connection_reset(self): + read_mock = MagicMock() + read_mock.side_effect = ConnectionResetError("Connection reset") + + connection = DummyConnection() + connection._read = read_mock + + with self.assertRaisesRegex( + GvmError, "^Remote closed the connection. Error was " + ): + connection.read() + + def test_send_not_connected(self): + connection = DummyConnection() + + with self.assertRaisesRegex(GvmError, "^Socket is not connected$"): + connection.send(b"") + + def test_send_remote_closed(self): + for error in ( + BrokenPipeError("Broken pipe"), + ConnectionResetError("Connection reset"), + ConnectionAbortedError("Connection aborted"), + ): + with self.subTest(error=type(error).__name__): + connection = DummyConnection() + connection._socket = MagicMock() + connection._socket.sendall.side_effect = error + + with self.assertRaisesRegex( + GvmError, "^Remote closed the connection. Error was " + ) as cm: + connection.send(b"") + + self.assertIs(cm.exception.__cause__, error) + + def test_send_other_os_error_is_not_wrapped(self): + connection = DummyConnection() + connection._socket = MagicMock() + connection._socket.sendall.side_effect = TimeoutError("timed out") + + with self.assertRaises(TimeoutError): + connection.send(b"") + def test_read_trigger_timeout(self): # mocking the response into two parts, so we run into the timeout # check in the loop