From b70395e831dd7efa023c8d77385adda4b26ef1a5 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Wed, 24 Jan 2024 13:23:22 +0100 Subject: [PATCH] handle invalid B2 server errors --- b2sdk/b2http.py | 22 +++++++++++--- .../+handle_invalid_error_format.fixed.md | 1 + test/unit/b2http/test_b2http.py | 30 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 changelog.d/+handle_invalid_error_format.fixed.md diff --git a/b2sdk/b2http.py b/b2sdk/b2http.py index c5fce9e8..07f87142 100644 --- a/b2sdk/b2http.py +++ b/b2sdk/b2http.py @@ -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". @@ -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, ) diff --git a/changelog.d/+handle_invalid_error_format.fixed.md b/changelog.d/+handle_invalid_error_format.fixed.md new file mode 100644 index 00000000..48893200 --- /dev/null +++ b/changelog.d/+handle_invalid_error_format.fixed.md @@ -0,0 +1 @@ +Handle json encoded, invalid B2 error responses, preventing exceptions such as `invalid literal for int() with base 10: 'service_unavailable'`. \ No newline at end of file diff --git a/test/unit/b2http/test_b2http.py b/test/unit/b2http/test_b2http.py index 936247a8..f9240678 100644 --- a/test/unit/b2http/test_b2http.py +++ b/test/unit/b2http/test_b2http.py @@ -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()