Add a retrying HTTP helper shared by every remote call
One place decides what is worth another attempt: 429, 5xx and network errors, with jittered exponential backoff and Retry-After honoured. Other 4xx responses fail at once. Downloads stream to disk under the same policy, refuse a body over the size cap, and only flickr calls and photo downloads are paced; the api key is redacted from debug output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+173
No files matched your search
@@ -236,3 +236,70 @@ class Settings(TestCase):
|
||||
def test_tilde_expanded_in_paths(self):
|
||||
path = self.write("s.yml", "directory: ~/photos\n")
|
||||
self.assertEqual(rb.loadsettings(path)["directory"], os.path.expanduser("~/photos"))
|
||||
|
||||
|
||||
class Retrying(TestCase):
|
||||
def test_success_returns_body(self):
|
||||
rb.OPEN = FakeOpen(FakeResponse(b"ok"))
|
||||
self.assertEqual(rb.fetch("http://t/x", "ctx"), b"ok")
|
||||
self.assertEqual(rb.OPEN.requests[0].get_header("User-agent"), rb.USER_AGENT)
|
||||
|
||||
def test_404_is_not_retried(self):
|
||||
rb.OPEN = FakeOpen(http_error(404, b"gone"))
|
||||
with self.assertRaises(rb.HTTPFailure) as cm:
|
||||
rb.fetch("http://t/x", "ctx")
|
||||
self.assertEqual(cm.exception.code, 404)
|
||||
self.assertIn("gone", str(cm.exception))
|
||||
self.assertEqual(len(rb.OPEN.requests), 1)
|
||||
|
||||
def test_5xx_and_429_retry_then_succeed(self):
|
||||
rb.OPEN = FakeOpen(http_error(503), http_error(429), FakeResponse(b"ok"))
|
||||
self.assertEqual(rb.fetch("http://t/x", "ctx"), b"ok")
|
||||
self.assertEqual(len(rb.OPEN.requests), 3)
|
||||
self.assertEqual(rb.STATS["retries"], 2)
|
||||
|
||||
def test_connection_errors_retry(self):
|
||||
rb.OPEN = FakeOpen(OSError("reset"), FakeResponse(b"ok"))
|
||||
self.assertEqual(rb.fetch("http://t/x", "ctx"), b"ok")
|
||||
|
||||
def test_gives_up_after_attempts(self):
|
||||
rb.ATTEMPTS = 3
|
||||
rb.OPEN = FakeOpen(http_error(500), http_error(500), http_error(500))
|
||||
with self.assertRaises(rb.HTTPFailure) as cm:
|
||||
rb.fetch("http://t/x", "ctx")
|
||||
self.assertIn("3 attempts", str(cm.exception))
|
||||
|
||||
def test_retry_after_header_is_honoured(self):
|
||||
waits = []
|
||||
rb.SLEEP = waits.append
|
||||
rb.OPEN = FakeOpen(http_error(429, headers={"Retry-After": "12"}), FakeResponse(b"ok"))
|
||||
rb.fetch("http://t/x", "ctx")
|
||||
self.assertIn(12.0, waits)
|
||||
|
||||
def test_backoff_grows_and_caps(self):
|
||||
rb.BACKOFF_BASE = 5
|
||||
self.assertLessEqual(rb.backoff(1), 7.5)
|
||||
self.assertGreaterEqual(rb.backoff(1), 2.5)
|
||||
self.assertLessEqual(rb.backoff(20), rb.BACKOFF_CAP * 1.5)
|
||||
|
||||
def test_post_sends_body_and_headers(self):
|
||||
rb.OPEN = FakeOpen(FakeResponse(b"{}"))
|
||||
rb.fetch("http://t/x", "ctx", headers={"x-api-key": "k"}, data=b"body", method="POST")
|
||||
req = rb.OPEN.requests[0]
|
||||
self.assertEqual(req.get_method(), "POST")
|
||||
self.assertEqual(req.data, b"body")
|
||||
self.assertEqual(req.get_header("X-api-key"), "k")
|
||||
|
||||
def test_download_streams_to_path(self):
|
||||
rb.OPEN = FakeOpen(FakeResponse(b"x" * 1000, headers={"Content-Type": "image/jpeg"}))
|
||||
path = os.path.join(self.dir, "p.part")
|
||||
self.assertEqual(rb.download("http://t/p.jpg", "ctx", path), ("image/jpeg", 1000))
|
||||
self.assertEqual(os.path.getsize(path), 1000)
|
||||
def test_download_refuses_an_oversized_body(self):
|
||||
rb.DOWNLOAD_CAP = 10
|
||||
rb.OPEN = FakeOpen(FakeResponse(b"x" * 100))
|
||||
path = os.path.join(self.dir, "p.part")
|
||||
with self.assertRaises(rb.BackupError) as cm:
|
||||
rb.download("http://t/p.jpg", "ctx", path)
|
||||
self.assertIn("10", str(cm.exception))
|
||||
self.assertEqual(len(rb.OPEN.requests), 1) # not a retryable failure
|
||||
Reference in new issue
Block a user