fix(agent): отрицательный Retry-After, обрыв при чтении тела ошибки

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
Andrey Epifancev 2026-10-11 23:31:50 +04:00
parent 1d5e128bd0
commit 24cfdcf25c
2 changed files with 28 additions and 3 deletions

View File

@ -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

View File

@ -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"}