Skip to content

Commit db8f032

Browse files
feat: enhance CheckoutApiException to include request_id and improve error handling (#186)
1 parent 9d50c61 commit db8f032

File tree

5 files changed

+89
-18
lines changed

5 files changed

+89
-18
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,15 @@ def oauth():
176176
## Exception handling
177177

178178
All the API responses that do not fall in the 2** status codes will cause a `CheckoutApiException`. The exception encapsulates
179-
the `http_metadata` and a dictionary of `error_details`, if available.
179+
the `http_metadata`, `request_id`, `error_type`, and a list of `error_details`, if available.
180180

181181
```python
182182
try:
183183
checkout_api.customers.get("customer_id")
184184
except CheckoutApiException as err:
185185
http_status_code = err.http_metadata.status_code
186+
request_id = err.request_id
187+
error_type = err.error_type
186188
error_details = err.error_details
187189
```
188190

checkout_sdk/exception.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import absolute_import
22

3+
import logging
4+
35
from checkout_sdk.authorization_type import AuthorizationType
46
from checkout_sdk.utils import map_to_http_metadata
57

@@ -33,21 +35,27 @@ def invalid_key(key_type: AuthorizationType):
3335

3436
class CheckoutApiException(CheckoutException):
3537
http_metadata: dict
38+
request_id: str
3639
error_details: list
3740
error_type: str
3841

3942
def __init__(self, response):
4043
self.http_metadata = map_to_http_metadata(response)
44+
self.request_id = None
45+
self.error_details = None
46+
self.error_type = None
47+
4148
if response.text:
4249
try:
4350
payload = response.json()
51+
self.request_id = payload.get('request_id')
4452
self.error_details = payload.get('error_codes')
4553
self.error_type = payload.get('error_type')
46-
except ValueError:
47-
self.error_details = None
48-
self.error_type = None
49-
else:
50-
self.error_details = None
51-
self.error_type = None
54+
except (ValueError, KeyError, TypeError) as e:
55+
logging.error("Failed to parse response JSON payload: %s", e)
56+
57+
if not self.request_id:
58+
self.request_id = response.headers.get('Cko-Request-Id')
59+
5260
super().__init__('The API response status code ({}) does not indicate success.'
5361
.format(response.status_code))

tests/checkout_test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class VisaCard:
117117
name: str = 'Checkout Test'
118118
number: str = '4242424242424242'
119119
expiry_month: int = 6
120-
expiry_year: int = 2025
120+
expiry_year: int = 2030
121121
cvv: str = '100'
122122

123123

tests/exception_test.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,75 +66,117 @@ def test_invalid_authorization_various_types(auth_type_name):
6666
def test_checkout_api_exception():
6767
response = Mock()
6868
response.status_code = 400
69-
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"]}'
69+
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"], "request_id": "req_123456"}'
7070
response.json.return_value = {
7171
"error_type": "request_invalid",
72-
"error_codes": ["invalid_field"]
72+
"error_codes": ["invalid_field"],
73+
"request_id": "req_123456"
7374
}
75+
response.headers = {}
7476

7577
with pytest.raises(CheckoutApiException) as exc_info:
7678
raise CheckoutApiException(response)
7779
exception = exc_info.value
7880
assert exception.http_metadata.status_code == 400
7981
assert exception.error_type == "request_invalid"
8082
assert exception.error_details == ["invalid_field"]
83+
assert exception.request_id == "req_123456"
8184

8285

8386
def test_checkout_api_exception_without_error_details():
8487
response = Mock()
8588
response.status_code = 500
86-
response.text = '{"message": "Internal Server Error"}'
89+
response.text = '{"message": "Internal Server Error", "request_id": "req_789012"}'
8790
response.json.return_value = {
88-
"message": "Internal Server Error"
91+
"message": "Internal Server Error",
92+
"request_id": "req_789012"
8993
}
94+
response.headers = {}
9095

9196
with pytest.raises(CheckoutApiException) as exc_info:
9297
raise CheckoutApiException(response)
9398
exception = exc_info.value
9499
assert exception.http_metadata.status_code == 500
95100
assert exception.error_type is None
96101
assert exception.error_details is None
102+
assert exception.request_id == "req_789012"
97103

98104

99105
def test_checkout_api_exception_empty_response():
100106
response = Mock()
101107
response.status_code = 404
102108
response.text = ''
103-
response.json.return_value = {}
109+
response.headers = {'Cko-Request-Id': 'header_req_345678'}
104110

105111
with pytest.raises(CheckoutApiException) as exc_info:
106112
raise CheckoutApiException(response)
107113
exception = exc_info.value
108114
assert exception.http_metadata.status_code == 404
109115
assert exception.error_type is None
110116
assert exception.error_details is None
117+
assert exception.request_id == "header_req_345678"
111118

112119

113120
def test_checkout_api_exception_non_json_response():
114121
response = Mock()
115122
response.status_code = 502
116123
response.text = 'Bad Gateway'
117124
response.json.side_effect = ValueError("No JSON object could be decoded")
125+
response.headers = {'Cko-Request-Id': 'header_req_502502'}
118126

119127
with pytest.raises(CheckoutApiException) as exc_info:
120128
raise CheckoutApiException(response)
121129
exception = exc_info.value
122130
assert exception.http_metadata.status_code == 502
123131
assert exception.error_type is None
124132
assert exception.error_details is None
133+
assert exception.request_id == "header_req_502502"
134+
135+
136+
def test_checkout_api_exception_request_id_from_header_fallback():
137+
response = Mock()
138+
response.status_code = 400
139+
response.text = '{"error_type": "request_invalid", "error_codes": ["invalid_field"]}'
140+
response.json.return_value = {
141+
"error_type": "request_invalid",
142+
"error_codes": ["invalid_field"]
143+
}
144+
response.headers = {'Cko-Request-Id': '0120e756-6d00-453c-a398-ff1643f9a873'}
145+
146+
with pytest.raises(CheckoutApiException) as exc_info:
147+
raise CheckoutApiException(response)
148+
exception = exc_info.value
149+
assert exception.request_id == "0120e756-6d00-453c-a398-ff1643f9a873"
150+
assert exception.error_type == "request_invalid"
151+
assert exception.error_details == ["invalid_field"]
152+
153+
154+
def test_checkout_api_exception_no_request_id_anywhere():
155+
response = Mock()
156+
response.status_code = 400
157+
response.text = '{"error_type": "request_invalid"}'
158+
response.json.return_value = {"error_type": "request_invalid"}
159+
response.headers = {} # Sin Cko-Request-Id
160+
161+
with pytest.raises(CheckoutApiException) as exc_info:
162+
raise CheckoutApiException(response)
163+
exception = exc_info.value
164+
assert exception.request_id is None
165+
assert exception.error_type == "request_invalid"
125166

126167

127168
@pytest.mark.parametrize("status_code", [400, 401, 403, 404, 500])
128169
def test_checkout_api_exception_various_status_codes(status_code):
129170
response = Mock()
130171
response.status_code = status_code
131172
response.text = ''
132-
response.json.return_value = {}
173+
response.headers = {'Cko-Request-Id': f'req_{status_code}'}
133174

134175
with pytest.raises(CheckoutApiException) as exc_info:
135176
raise CheckoutApiException(response)
136177
exception = exc_info.value
137178
assert exception.http_metadata.status_code == status_code
179+
assert exception.request_id == f'req_{status_code}'
138180

139181

140182
def test_map_to_http_metadata():
@@ -150,28 +192,46 @@ def test_map_to_http_metadata():
150192
def test_checkout_api_exception_message():
151193
response = Mock()
152194
response.status_code = 400
153-
response.text = '{"error_type": "invalid_request", "error_codes": ["bad_request"]}'
195+
response.text = '{"error_type": "invalid_request", "error_codes": ["bad_request"], "request_id": "msg_req_400"}'
154196
response.json.return_value = {
155197
"error_type": "invalid_request",
156-
"error_codes": ["bad_request"]
198+
"error_codes": ["bad_request"],
199+
"request_id": "msg_req_400"
157200
}
201+
response.headers = {}
158202

159203
with pytest.raises(CheckoutApiException) as exc_info:
160204
raise CheckoutApiException(response)
161205
exception = exc_info.value
162206
expected_message = "The API response status code (400) does not indicate success."
163207
assert str(exception) == expected_message
208+
assert exception.request_id == "msg_req_400"
164209

165210

166211
def test_checkout_api_exception_no_response_text():
167212
response = Mock()
168213
response.status_code = 400
169214
response.text = None
170-
response.json.return_value = {}
215+
response.headers = {'Cko-Request-Id': 'no_text_req_id'}
171216

172217
with pytest.raises(CheckoutApiException) as exc_info:
173218
raise CheckoutApiException(response)
174219
exception = exc_info.value
175220
assert exception.http_metadata.status_code == 400
176221
assert exception.error_type is None
177222
assert exception.error_details is None
223+
assert exception.request_id == "no_text_req_id"
224+
225+
226+
def test_checkout_api_exception_logs_on_json_parse_error(caplog):
227+
response = Mock()
228+
response.status_code = 502
229+
response.text = 'Bad Gateway'
230+
response.json.side_effect = ValueError("No JSON object could be decoded")
231+
response.headers = {'Cko-Request-Id': 'header_req_logging'}
232+
233+
with caplog.at_level("ERROR"):
234+
with pytest.raises(CheckoutApiException):
235+
raise CheckoutApiException(response)
236+
237+
assert any("Failed to parse response JSON payload" in m for m in caplog.messages)

tests/payments/request_apm_payments_integration_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ def test_should_request_alipay_plus_payment(default_api):
223223
except CheckoutApiException as err:
224224
assert err.args[0] == 'The API response status code (422) does not indicate success.'
225225
assert err.error_type == 'invalid_request'
226-
assert err.error_details[0] == 'reference_invalid'
226+
assert err.error_details is not None and 'reference_invalid' in err.error_details
227+
assert err.request_id is not None
227228

228229

229230
def test_should_make_przelewy24_payment(default_api):

0 commit comments

Comments
 (0)