From 56c080073459ee27718c7c61f598d437a3726823 Mon Sep 17 00:00:00 2001 From: Omarchybot <317366263+omarchybot@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:34:41 +0200 Subject: [PATCH] Hold an error reply's body to the same deadline as a success open_json read a successful body through _read_by, bounded by three times the socket timeout, but an HTTP error status read its body with error.read(), which only bounds each wait. A provider answering 429 or 500 and dripping its body held the collector, and the updater's wait held every later refresh behind it. Past the deadline the body is left out and the status still answers. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Codex Medium --- shell/plugins/agents/collectors.py | 3 ++- test/shell.d/agent-usage-shared-test.sh | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/shell/plugins/agents/collectors.py b/shell/plugins/agents/collectors.py index 15113e88fa9..cb33ba526fb 100644 --- a/shell/plugins/agents/collectors.py +++ b/shell/plugins/agents/collectors.py @@ -3576,8 +3576,9 @@ def open_json(request: urllib.request.Request, timeout: float = 10, limit: int = # its Content-Length, with the rest still owed. owed = getattr(response, "length", None) except urllib.error.HTTPError as error: + # An error's body drips like any other; past the deadline it is left out. try: - body = error.read(limit) or b"" + body = _read_by(error.fp, limit, deadline) if error.fp else b"" except Exception: body = b"" raise HttpError(status=error.code, body=body, headers=error.headers) from error diff --git a/test/shell.d/agent-usage-shared-test.sh b/test/shell.d/agent-usage-shared-test.sh index 1f40dea5212..9a943dae535 100644 --- a/test/shell.d/agent-usage-shared-test.sh +++ b/test/shell.d/agent-usage-shared-test.sh @@ -460,7 +460,7 @@ except collectors.HttpError as error: check("a body short of its Content-Length over a socket is cut short", short is not None and (short.transport, short.invalid, short.cut_short) == (False, True, True), repr(short and vars(short))) -def drip_server(): +def drip_server(status=b"200 OK"): listener = socket.socket() listener.bind(("127.0.0.1", 0)) listener.listen(1) @@ -469,7 +469,7 @@ def drip_server(): conn, _ = listener.accept() with conn: conn.recv(65536) - conn.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 1000\r\n\r\n') + conn.sendall(b'HTTP/1.1 ' + status + b'\r\nContent-Type: application/json\r\nContent-Length: 1000\r\n\r\n') with contextlib.suppress(OSError): for _ in range(100): conn.sendall(b" ") @@ -491,6 +491,17 @@ check( dripped is not None and dripped.cut_short and not dripped.transport and time.monotonic() - began < 5, repr(dripped and vars(dripped)), ) +began = time.monotonic() +try: + collectors.open_json(urllib.request.Request(drip_server(b"500 Internal Server Error")), timeout=0.3) + dripped = None +except collectors.HttpError as error: + dripped = error +check( + "an error reply dripping its body still answers with its status by the deadline", + dripped is not None and dripped.status == 500 and time.monotonic() - began < 5, + repr(dripped and vars(dripped)), +) bad_key = urllib.request.Request("https://example.invalid", headers={"Authorization": "Bearer sk-secret\nX: y"}) urllib.request.urlopen = real_urlopen try: