diff --git a/swh/core/api/__init__.py b/swh/core/api/__init__.py --- a/swh/core/api/__init__.py +++ b/swh/core/api/__init__.py @@ -12,7 +12,7 @@ import requests import datetime -from typing import ClassVar, Optional, Type +from typing import Any, ClassVar, Optional, Type from flask import Flask, Request, Response, request, abort from .serializers import (decode_response, @@ -76,7 +76,20 @@ # base API classes class RemoteException(Exception): - pass + """raised when remote returned an out-of-band failure notification, e.g., as a + HTTP status code or serialized exception + + Attributes: + response: HTTP response corresponding to the failure + + """ + def __init__(self, payload: Optional[Any] = None, + response: Optional[requests.Response] = None): + if payload is not None: + super().__init__(payload) + else: + super().__init__() + self.response = response def remote_api_endpoint(path): @@ -201,6 +214,7 @@ **opts) if opts.get('stream') or \ response.headers.get('transfer-encoding') == 'chunked': + self.raise_for_status(response) return response.iter_content(chunk_size) else: return self._decode_response(response) @@ -215,6 +229,7 @@ **opts) if opts.get('stream') or \ response.headers.get('transfer-encoding') == 'chunked': + self.raise_for_status(response) return response.iter_content(chunk_size) else: return self._decode_response(response) @@ -222,28 +237,46 @@ def get_stream(self, endpoint, **opts): return self.get(endpoint, stream=True, **opts) + def raise_for_status(self, response) -> None: + """check response HTTP status code and raise an exception if it denotes an + error; do nothing otherwise + + """ + # XXX: unpickling below breaks language-independence and should be + # replaced by proper language-independent [de]serialization + status_code = response.status_code + status_class = response.status_code // 100 + + if status_code == 404: + raise RemoteException(payload='404 not found', response=response) + + try: + if status_class == 4: + data = decode_response(response) + raise pickle.loads(data) + + if status_class == 5: + data = decode_response(response) + if 'exception_pickled' in data: + raise pickle.loads(data['exception_pickled']) + else: + raise RemoteException(payload=data['exception'], + response=response) + + except (TypeError, pickle.UnpicklingError): + raise RemoteException(payload=data, response=response) + + if status_class != 2: + raise RemoteException( + payload=f'API HTTP error: {status_code} {response.content}', + response=response) + def _decode_response(self, response): if response.status_code == 404: return None - if response.status_code == 500: - data = decode_response(response) - if 'exception_pickled' in data: - raise pickle.loads(data['exception_pickled']) - else: - raise RemoteException(data['exception']) - - # XXX: this breaks language-independence and should be - # replaced by proper unserialization - if response.status_code == 400: - raise pickle.loads(decode_response(response)) - elif response.status_code != 200: - raise RemoteException( - "Unexpected status code for API request: %s (%s)" % ( - response.status_code, - response.content, - ) - ) - return decode_response(response) + else: + self.raise_for_status(response) + return decode_response(response) def __repr__(self): return '<{} url={}>'.format(self.__class__.__name__, self.url) diff --git a/swh/core/api/serializers.py b/swh/core/api/serializers.py --- a/swh/core/api/serializers.py +++ b/swh/core/api/serializers.py @@ -29,6 +29,8 @@ r = msgpack_loads(response.content) elif content_type.startswith('application/json'): r = json.loads(response.text, cls=SWHJSONDecoder) + elif content_type.startswith('text/'): + r = response.text else: raise ValueError('Wrong content type `%s` for API response' % content_type)