diff --git a/gocardless_pro/api_client.py b/gocardless_pro/api_client.py index d888496c..27c52e11 100644 --- a/gocardless_pro/api_client.py +++ b/gocardless_pro/api_client.py @@ -126,11 +126,18 @@ def _handle_errors(self, response): response_body = response.json() except ValueError: msg = 'Malformed response received from server' - raise errors.MalformedResponseError(msg, response.text) + raise errors.MalformedResponseError(msg, response.text, response.status_code) if response.status_code < 400: return + if not isinstance(response_body, dict): + raise errors.MalformedResponseError( + 'Malformed response received from server', + response.text, + response.status_code, + ) + error = response_body.get('error', response_body) if isinstance(error, str): @@ -139,8 +146,14 @@ def _handle_errors(self, response): 'message': error, } exception_class = errors.ApiError - else: + elif isinstance(error, dict) and 'type' in error: exception_class = errors.ApiError.exception_for(response.status_code, error['type'], error.get('errors')) + else: + raise errors.MalformedResponseError( + 'Malformed response received from server', + response.text, + response.status_code, + ) raise exception_class(error) diff --git a/gocardless_pro/errors.py b/gocardless_pro/errors.py index 6688e374..f37ee6fc 100644 --- a/gocardless_pro/errors.py +++ b/gocardless_pro/errors.py @@ -137,9 +137,26 @@ class RateLimitError(ApiError): class MalformedResponseError(GoCardlessProError): - def __init__(self, message, response): - super(MalformedResponseError, self).__init__(message) + BODY_PREVIEW_MAX_LENGTH = 500 + + def __init__(self, message, response, status_code=None): + self.status_code = status_code self.response = response + super(MalformedResponseError, self).__init__( + self._build_message(message, status_code, response) + ) + + @classmethod + def _build_message(cls, message, status_code, response): + parts = [message] + if status_code is not None: + parts.append('(HTTP {})'.format(status_code)) + full = ' '.join(parts) + if response: + preview = response if len(response) <= cls.BODY_PREVIEW_MAX_LENGTH \ + else response[:cls.BODY_PREVIEW_MAX_LENGTH] + '...' + full = '{}: {}'.format(full, preview) + return full class InvalidSignatureError(GoCardlessProError): diff --git a/gocardless_pro/services/billing_request_flows_service.py b/gocardless_pro/services/billing_request_flows_service.py index 96fe3f65..804a9e03 100644 --- a/gocardless_pro/services/billing_request_flows_service.py +++ b/gocardless_pro/services/billing_request_flows_service.py @@ -58,7 +58,7 @@ def initialise(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/billing_requests_service.py b/gocardless_pro/services/billing_requests_service.py index 75485e95..2e94bbc9 100644 --- a/gocardless_pro/services/billing_requests_service.py +++ b/gocardless_pro/services/billing_requests_service.py @@ -75,7 +75,7 @@ def collect_customer_details(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -126,7 +126,7 @@ def collect_bank_account(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -153,7 +153,7 @@ def confirm_payer_details(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -179,7 +179,7 @@ def fulfil(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -205,7 +205,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -284,7 +284,7 @@ def notify(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -309,7 +309,7 @@ def fallback(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -339,7 +339,7 @@ def choose_currency(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -363,7 +363,7 @@ def select_institution(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/blocks_service.py b/gocardless_pro/services/blocks_service.py index b57e1a24..2b4cb044 100644 --- a/gocardless_pro/services/blocks_service.py +++ b/gocardless_pro/services/blocks_service.py @@ -113,7 +113,7 @@ def disable(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -138,7 +138,7 @@ def enable(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -162,7 +162,7 @@ def block_by_ref(self,params=None, headers=None): path = '/blocks/block_by_ref' if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/creditor_bank_accounts_service.py b/gocardless_pro/services/creditor_bank_accounts_service.py index b9ca1893..9237e510 100644 --- a/gocardless_pro/services/creditor_bank_accounts_service.py +++ b/gocardless_pro/services/creditor_bank_accounts_service.py @@ -120,7 +120,7 @@ def disable(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/customer_bank_accounts_service.py b/gocardless_pro/services/customer_bank_accounts_service.py index ccc0c9f0..2dbec878 100644 --- a/gocardless_pro/services/customer_bank_accounts_service.py +++ b/gocardless_pro/services/customer_bank_accounts_service.py @@ -157,7 +157,7 @@ def disable(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/customer_notifications_service.py b/gocardless_pro/services/customer_notifications_service.py index 45b92e20..3e59af4d 100644 --- a/gocardless_pro/services/customer_notifications_service.py +++ b/gocardless_pro/services/customer_notifications_service.py @@ -43,7 +43,7 @@ def handle(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/funds_availabilities_service.py b/gocardless_pro/services/funds_availabilities_service.py index c8b80b96..72af7fdc 100644 --- a/gocardless_pro/services/funds_availabilities_service.py +++ b/gocardless_pro/services/funds_availabilities_service.py @@ -14,7 +14,7 @@ class FundsAvailabilitiesService(base_service.BaseService): """ RESOURCE_CLASS = resources.FundsAvailability - RESOURCE_NAME = 'funds_availabilities' + RESOURCE_NAME = 'funds_availability' def check(self,identity,params=None, headers=None): diff --git a/gocardless_pro/services/instalment_schedules_service.py b/gocardless_pro/services/instalment_schedules_service.py index a24fc662..98bd28d0 100644 --- a/gocardless_pro/services/instalment_schedules_service.py +++ b/gocardless_pro/services/instalment_schedules_service.py @@ -205,7 +205,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/mandate_imports_service.py b/gocardless_pro/services/mandate_imports_service.py index 748cb113..58b863de 100644 --- a/gocardless_pro/services/mandate_imports_service.py +++ b/gocardless_pro/services/mandate_imports_service.py @@ -103,7 +103,7 @@ def submit(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -133,7 +133,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/mandates_service.py b/gocardless_pro/services/mandates_service.py index a7d5dcc8..30bb173d 100644 --- a/gocardless_pro/services/mandates_service.py +++ b/gocardless_pro/services/mandates_service.py @@ -143,7 +143,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -178,7 +178,7 @@ def reinstate(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/outbound_payments_service.py b/gocardless_pro/services/outbound_payments_service.py index db35614d..1ecaa82e 100644 --- a/gocardless_pro/services/outbound_payments_service.py +++ b/gocardless_pro/services/outbound_payments_service.py @@ -60,7 +60,7 @@ def withdraw(self,params=None, headers=None): path = '/outbound_payments/withdrawal' if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -88,7 +88,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -113,7 +113,7 @@ def approve(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/payer_authorisations_service.py b/gocardless_pro/services/payer_authorisations_service.py index ec7beddb..d99f0988 100644 --- a/gocardless_pro/services/payer_authorisations_service.py +++ b/gocardless_pro/services/payer_authorisations_service.py @@ -129,7 +129,7 @@ def submit(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -164,7 +164,7 @@ def confirm(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/payments_service.py b/gocardless_pro/services/payments_service.py index 3a736ee0..c84c69ab 100644 --- a/gocardless_pro/services/payments_service.py +++ b/gocardless_pro/services/payments_service.py @@ -149,7 +149,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -182,7 +182,7 @@ def retry(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/redirect_flows_service.py b/gocardless_pro/services/redirect_flows_service.py index 7aeb4e36..73f98d66 100644 --- a/gocardless_pro/services/redirect_flows_service.py +++ b/gocardless_pro/services/redirect_flows_service.py @@ -97,7 +97,7 @@ def complete(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/scenario_simulators_service.py b/gocardless_pro/services/scenario_simulators_service.py index 2b9ddebc..6f40aa21 100644 --- a/gocardless_pro/services/scenario_simulators_service.py +++ b/gocardless_pro/services/scenario_simulators_service.py @@ -65,7 +65,7 @@ def run(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/subscriptions_service.py b/gocardless_pro/services/subscriptions_service.py index be5d3c84..dff2cc57 100644 --- a/gocardless_pro/services/subscriptions_service.py +++ b/gocardless_pro/services/subscriptions_service.py @@ -206,7 +206,7 @@ def pause(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -246,7 +246,7 @@ def resume(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) @@ -275,7 +275,7 @@ def cancel(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/gocardless_pro/services/webhooks_service.py b/gocardless_pro/services/webhooks_service.py index 9a12982a..1fed0f12 100644 --- a/gocardless_pro/services/webhooks_service.py +++ b/gocardless_pro/services/webhooks_service.py @@ -85,7 +85,7 @@ def retry(self,identity,params=None, headers=None): }) if params is not None: - params = {'data': params} + params = {self._envelope_key(): params} response = self._perform_request('POST', path, params, headers, retry_failures=False) return self._resource_for(response) diff --git a/tests/api_client_test.py b/tests/api_client_test.py index 6da7044b..53509d3e 100644 --- a/tests/api_client_test.py +++ b/tests/api_client_test.py @@ -125,6 +125,19 @@ def test_handles_malformed_response(): with pytest.raises(errors.MalformedResponseError): client.post('/test', body={'name': 'Billy Jean'}) +@responses.activate +def test_includes_status_code_and_body_for_non_json_error_response(): + body = '502 Bad Gateway' + responses.add(responses.POST, 'http://example.com/test', + body=body, status=502) + + with pytest.raises(errors.MalformedResponseError) as exception: + client.post('/test', body={'name': 'Billy Jean'}) + + assert exception.value.status_code == 502 + assert exception.value.response == body + assert 'HTTP 502' in str(exception.value) + @responses.activate def test_handles_valid_empty_response(): responses.add(responses.DELETE, 'http://example.com/test', body='', status=204)