diff --git a/src/py/mat3ra/api_client/utils/http.py b/src/py/mat3ra/api_client/utils/http.py index 73b80de..c91de1e 100644 --- a/src/py/mat3ra/api_client/utils/http.py +++ b/src/py/mat3ra/api_client/utils/http.py @@ -2,6 +2,15 @@ import urllib.parse +def _extract_server_message(response: requests.Response) -> str: + """Extract human-readable message from a JSEND-formatted error response body.""" + try: + body = response.json() + return body.get("data", {}).get("message") or body.get("message") or "" + except Exception: + return "" + + class BaseConnection(object): """ Base connection class to inherit from. This class should not be instantiated directly. @@ -32,7 +41,14 @@ def request(self, method, url, params=None, data=None, headers=None): params (dict): URL parameters to append to the URL. """ self.response = self.session.request(method=method.lower(), url=url, params=params, data=data, headers=headers) - self.response.raise_for_status() + try: + self.response.raise_for_status() + except requests.HTTPError: + status_code = self.response.status_code + server_message = _extract_server_message(self.response) + detail = server_message or "HTTP Error" + message = f"Error {status_code}: {detail}." + raise requests.HTTPError(message, response=self.response) from None def get_response(self): """ diff --git a/tests/py/unit/test_httpBase.py b/tests/py/unit/test_httpBase.py index be3a8dc..82c92ed 100644 --- a/tests/py/unit/test_httpBase.py +++ b/tests/py/unit/test_httpBase.py @@ -1,3 +1,4 @@ +import json from unittest import mock from mat3ra.api_client.utils.http import Connection @@ -7,11 +8,10 @@ API_VERSION_1 = "2018-10-1" API_VERSION_2 = "2018-10-2" HTTP_STATUS_UNAUTHORIZED = 401 -HTTP_REASON_UNAUTHORIZED = "Unauthorized" +HTTP_STATUS_UNKNOWN = 418 EMPTY_CONTENT = "" -TEST_ENTITY_ID = "28FMvD5knJZZx452H" -EMPTY_USERNAME = "" -EMPTY_PASSWORD = "" +SERVER_MESSAGE = "Custom server error message" +SERVER_ERROR_RESPONSE = json.dumps({"message": SERVER_MESSAGE}) class HTTPBaseUnitTest(EndpointBaseUnitTest): @@ -36,8 +36,26 @@ def test_preamble_version(self): @mock.patch("requests.sessions.Session.request") def test_raise_http_error(self, mock_request): - mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNAUTHORIZED, - reason=HTTP_REASON_UNAUTHORIZED) + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNAUTHORIZED) with self.assertRaises(HTTPError): conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) - conn.request("POST", "login", data={"username": EMPTY_USERNAME, "password": EMPTY_PASSWORD}) + conn.request("POST", "login") + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_with_server_message(self, mock_request): + mock_request.return_value = self.mock_response(SERVER_ERROR_RESPONSE, HTTP_STATUS_UNAUTHORIZED) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("POST", "login") + self.assertIn("Error 401", str(ctx.exception)) + self.assertIn(SERVER_MESSAGE, str(ctx.exception)) + + @mock.patch("requests.sessions.Session.request") + def test_http_error_message_without_server_message(self, mock_request): + mock_request.return_value = self.mock_response(EMPTY_CONTENT, HTTP_STATUS_UNKNOWN) + with self.assertRaises(HTTPError) as ctx: + conn = Connection(self.host, self.port, version=API_VERSION_1, secure=True) + conn.request("GET", "materials") + self.assertIn("Error 418", str(ctx.exception)) + self.assertIn("HTTP Error", str(ctx.exception)) +