From 24cfdcf25c3fb1c3eda0698a11a76107cfa3b36d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 23:31:50 +0400 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=D0=BE=D1=82=D1=80=D0=B8=D1=86?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20Retry-After,?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D1=8B=D0=B2=20=D0=BF=D1=80=D0=B8=20=D1=87?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=82=D0=B5=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- src/agent.py | 8 +++++--- tests/test_clients.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/agent.py b/src/agent.py index 2bf1d02..6e02162 100644 --- a/src/agent.py +++ b/src/agent.py @@ -86,12 +86,14 @@ class Http: raw = resp.read() status = resp.status except urllib.error.HTTPError as e: + self._touch() try: - text = e.read().decode("utf-8", "replace") headers = {k.lower(): v for k, v in e.headers.items()} + text = e.read().decode("utf-8", "replace") + except (OSError, ConnectionError, TimeoutError) as err: + raise ConnectionError("%s: обрыв при чтении ответа: %s" % (self.base, err)) from None finally: e.close() - self._touch() raise HttpError(e.code, text, headers) from None except (urllib.error.URLError, ConnectionError, TimeoutError, OSError) as e: raise ConnectionError("%s недоступен: %s" % (self.base, e)) from None @@ -149,7 +151,7 @@ class JiraClient(Http): if e.code in (429, 503) and attempt < 5: wait = 60 * attempt try: - wait = int(e.headers.get("retry-after")) + wait = max(0, int(e.headers.get("retry-after"))) except (TypeError, ValueError): pass self.rate_limited += 1 diff --git a/tests/test_clients.py b/tests/test_clients.py index 70963a7..eb1ae8b 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -66,6 +66,29 @@ class ClientsTest(unittest.TestCase): self.jira.get("/rest/api/2/issue/AURORA-1") self.assertEqual(waits, [60]) + def test_retry_after_negative(self): + waits = [] + self.jira.sleep = waits.append + self.fake.add("AURORA-1") + self.fake.fail_next.append((429, {"Retry-After": "-5"})) + self.jira.get("/rest/api/2/issue/AURORA-1") + self.assertEqual(waits, [0]) + + def test_error_body_read_failure_is_connection_error(self): + import io + import urllib.error + from unittest import mock + + class Broken(io.BytesIO): + def read(self, *a): + raise TimeoutError("timed out") + + err = urllib.error.HTTPError("http://x", 500, "boom", {}, Broken()) + with mock.patch("urllib.request.urlopen", side_effect=err): + with self.assertRaises(ConnectionError): + self.jira.get("/x") + self.assertEqual(self.jira.calls, 1) + def test_bearer_header(self): cfg = jira_cfg(self.fake) cfg["jira"]["auth"] = {"type": "bearer", "token": "tok123"}