Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle invalid B2 server errors #463

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions b2sdk/b2http.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,9 @@ def _translate_errors(cls, fcn, post_params=None):
# Decode the error object returned by the service
try:
error = json.loads(response.content.decode('utf-8')) if response.content else {}
except (json.JSONDecodeError, UnicodeDecodeError):
if not isinstance(error, dict):
raise ValueError('json error value is not a dict')
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
logger.error('failed to decode error response: %r', response.content)
# When the user points to an S3 endpoint, he won't receive the JSON error
# he expects. In that case, we can provide at least a hint of "what happened".
Expand All @@ -439,10 +441,22 @@ def _translate_errors(cls, fcn, post_params=None):
logger.debug(
'received error has extra (unsupported) keys: %s', extra_error_keys
)

try:
status = int(error.get('status', response.status_code))
if status != response.status_code:
raise ValueError('status code is not equal to the one in the response')
except (TypeError, ValueError) as exc:
logger.warning(
'Inconsistent status codes returned by the server %r != %r; parsing exception: %r',
error.get('status'), response.status_code, exc
)
status = response.status_code

raise interpret_b2_error(
int(error.get('status', response.status_code)),
error.get('code'),
error.get('message'),
status,
str(error['code']) if 'code' in error else None,
str(error['message']) if 'message' in error else None,
response.headers,
post_params,
)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/+handle_invalid_error_format.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle json encoded, invalid B2 error responses, preventing exceptions such as `invalid literal for int() with base 10: 'service_unavailable'`.
30 changes: 30 additions & 0 deletions test/unit/b2http/test_b2http.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,36 @@ def test_b2_error__nginx_html():
assert response.content.decode('utf-8') in str(exc_info.value)


def test_b2_error__invalid_error_format():
"""
Handling of invalid error format.
If server returns valid JSON, but not matching B2 error schema, we should still raise ServiceError.
"""
response = MagicMock()
response.status_code = 503
# valid JSON, but not a valid B2 error (it should be a dict, not a list)
response.content = b'[]'
with pytest.raises(ServiceError) as exc_info:
B2Http._translate_errors(lambda: response)
assert '503' in str(exc_info.value)


def test_b2_error__invalid_error_values():
"""
Handling of invalid error values.
If server returns valid JSON, but not matching B2 error schema, we should still raise ServiceError.
"""
response = MagicMock()
response.status_code = 503
# valid JSON, but not a valid B2 error (code and status values (and therefore types!) are swapped)
response.content = b'{"code": 503, "message": "Service temporarily unavailable", "status": "service_unavailable"}'
with pytest.raises(ServiceError) as exc_info:
B2Http._translate_errors(lambda: response)
assert '503 Service temporarily unavailable' in str(exc_info.value)


class TestTranslateAndRetry(TestBase):
def setUp(self):
self.response = MagicMock()
Expand Down
Loading