From b52fdb0867d5d6319890bc4c4a92fdc16b3bb476 Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Wed, 30 Sep 2026 13:19:12 -0600 Subject: [PATCH] Honor Retry-After on 5xx responses, not just 429 A 5xx carrying Retry-After was ignored: the client retried on its default backoff instead of waiting the requested time, unlike 429. Give ServiceError a retry_after_seconds slot (mirroring TooManyRequests) and pass the header from the 5xx branch of interpret_b2_error; b2http already honors a non-None value. --- b2sdk/_internal/exception.py | 11 ++++++++++- changelog.d/606.fixed.md | 1 + test/unit/test_exception.py | 9 +++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 changelog.d/606.fixed.md diff --git a/b2sdk/_internal/exception.py b/b2sdk/_internal/exception.py index aef410f37..d692a778c 100644 --- a/b2sdk/_internal/exception.py +++ b/b2sdk/_internal/exception.py @@ -433,6 +433,12 @@ class ServiceError(TransientErrorMixin, B2Error): Used for HTTP status codes 500 through 599. """ + def __init__(self, *args, retry_after_seconds=None): + super().__init__(*args) + + if retry_after_seconds is not None: + self.retry_after_seconds = int(retry_after_seconds) + class CapExceeded(B2Error): def __str__(self): @@ -753,5 +759,8 @@ def interpret_b2_error( elif status == 429: return TooManyRequests(retry_after_seconds=response_headers.get('retry-after')) elif 500 <= status < 600: - return ServiceError('%d %s %s' % (status, code, message)) + return ServiceError( + '%d %s %s' % (status, code, message), + retry_after_seconds=response_headers.get('retry-after'), + ) return UnknownError('%d %s %s' % (status, code, message)) diff --git a/changelog.d/606.fixed.md b/changelog.d/606.fixed.md new file mode 100644 index 000000000..bd710ff39 --- /dev/null +++ b/changelog.d/606.fixed.md @@ -0,0 +1 @@ +Fixed honoring the Retry-After header on 5xx service errors. diff --git a/test/unit/test_exception.py b/test/unit/test_exception.py index 258e301a4..f4f6e8c96 100644 --- a/test/unit/test_exception.py +++ b/test/unit/test_exception.py @@ -147,6 +147,15 @@ def test_too_many_requests_without_retry_after_header(self): error = self._check_one(TooManyRequests, 429, '', '', {}) assert error.retry_after_seconds is None + def test_service_error_with_retry_after_header(self): + retry_after = 200 + error = self._check_one(ServiceError, 503, '', '', {'retry-after': retry_after}) + assert error.retry_after_seconds == retry_after + + def test_service_error_without_retry_after_header(self): + error = self._check_one(ServiceError, 503, '', '', {}) + assert error.retry_after_seconds is None + @pytest.mark.apiver( from_ver=3 ) # previous apivers throw this as well, but BucketIdNotFound is a different class in them