From 08815e6c7434cf57edf6eaf36e5a3014063f0cc5 Mon Sep 17 00:00:00 2001 From: honor2030 <20864931+honor2030@users.noreply.github.com> Date: Tue, 12 May 2026 13:39:51 +0900 Subject: [PATCH] fix: handle empty IPC responses --- src/browser_harness/_ipc.py | 10 +++++++--- src/browser_harness/admin.py | 2 ++ tests/unit/test_admin.py | 20 ++++++++++++++++++++ tests/unit/test_ipc.py | 24 ++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/browser_harness/_ipc.py b/src/browser_harness/_ipc.py index 2d265766..ba7f8538 100644 --- a/src/browser_harness/_ipc.py +++ b/src/browser_harness/_ipc.py @@ -91,15 +91,19 @@ def connect(name, timeout=1.0): def request(c, token, req): """One-shot send + recv + parse on an open socket. Injects token on Windows. - Returns the parsed JSON response. Caller closes the socket.""" + Returns the parsed JSON response. Caller closes the socket. + Raises ConnectionError if the peer closes before sending any response.""" if token: req = {**req, "token": token} c.sendall((json.dumps(req) + "\n").encode()) data = b"" while not data.endswith(b"\n"): chunk = c.recv(1 << 16) - if not chunk: break + if not chunk: + if not data: + raise ConnectionError("IPC peer closed without a response") + break data += chunk - return json.loads(data or b"{}") + return json.loads(data) def ping(name, timeout=1.0): diff --git a/src/browser_harness/admin.py b/src/browser_harness/admin.py index f4b634d0..da3049a2 100644 --- a/src/browser_harness/admin.py +++ b/src/browser_harness/admin.py @@ -191,6 +191,8 @@ def _daemon_browser_connection(name): try: c, token = ipc.connect(name, timeout=1.0) response = ipc.request(c, token, {"meta": "connection_status"}) + if not isinstance(response, dict): + return None if "error" in response: return None page = response.get("page") diff --git a/tests/unit/test_admin.py b/tests/unit/test_admin.py index 6826b399..5872d4f9 100644 --- a/tests/unit/test_admin.py +++ b/tests/unit/test_admin.py @@ -123,6 +123,26 @@ def test_browser_connections_returns_attached_page(monkeypatch): ] +def test_browser_connections_skips_daemon_that_closes_without_response(monkeypatch): + monkeypatch.setattr(admin, "_daemon_endpoint_names", lambda: ["default", "stale"]) + + def fake_connect(name, timeout=1.0): + if name == "stale": + return FakeSocket(b""), None + return FakeSocket(), None + + monkeypatch.setattr(admin.ipc, "connect", fake_connect) + + assert admin.browser_connections() == [{"name": "default", "page": None}] + + +def test_browser_connections_skips_non_dict_connection_status_response(monkeypatch): + monkeypatch.setattr(admin, "_daemon_endpoint_names", lambda: ["stale"]) + monkeypatch.setattr(admin.ipc, "connect", lambda name, timeout=1.0: (FakeSocket(b"[]\n"), None)) + + assert admin.browser_connections() == [] + + def test_chrome_running_detects_helium_on_linux(monkeypatch): monkeypatch.setattr("platform.system", lambda: "Linux") monkeypatch.setattr( diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index 96e2dbc6..28d64cec 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -1,6 +1,30 @@ +import pytest + from browser_harness import _ipc as ipc +# --- request(): EOF handling --- + +class _EOFConn: + def __init__(self): + self.sent = b"" + + def sendall(self, data): + self.sent += data + + def recv(self, _size): + return b"" + + +def test_request_raises_connection_error_when_peer_closes_without_response(): + conn = _EOFConn() + + with pytest.raises(ConnectionError, match="closed without a response"): + ipc.request(conn, None, {"meta": "ping"}) + + assert conn.sent + + # --- identify(): ping payload sanitation --- class _FakeConn: