diff --git a/b2sdk/_internal/exception.py b/b2sdk/_internal/exception.py index aef410f37..2b68753fc 100644 --- a/b2sdk/_internal/exception.py +++ b/b2sdk/_internal/exception.py @@ -433,6 +433,11 @@ 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 +758,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..996093341 --- /dev/null +++ b/changelog.d/606.fixed.md @@ -0,0 +1 @@ +Honor Retry-After headers when retrying B2 service errors (HTTP 5xx). 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