Skip to content

Commit 72970cd

Browse files
committed
feat: improve error responses
Create _error_response_hook function to augment error messages. Add hook to the client requests session. Unit tests for the hook.
1 parent ff3ac6e commit 72970cd

File tree

3 files changed

+529
-4
lines changed

3 files changed

+529
-4
lines changed

ibmcloudant/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# coding: utf-8
2-
# © Copyright IBM Corporation 2020, 2021.
2+
# © Copyright IBM Corporation 2020, 2024.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.

ibmcloudant/cloudant_base_service.py

+73-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# coding: utf-8
22

3-
# © Copyright IBM Corporation 2020, 2022.
3+
# © Copyright IBM Corporation 2020, 2024.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -15,12 +15,17 @@
1515
# limitations under the License.
1616
"""
1717
Module to patch sdk core base service for session authentication
18+
and other helpful features.
1819
"""
1920
from collections import namedtuple
2021
from typing import Dict, Optional, Union, Tuple, List
2122
from urllib.parse import urlsplit, unquote
23+
from json import dumps
24+
from json.decoder import JSONDecodeError
25+
from io import BytesIO
2226

2327
from ibm_cloud_sdk_core.authenticators import Authenticator
28+
from requests import Response
2429
from requests.cookies import RequestsCookieJar
2530

2631
from .common import get_sdk_headers
@@ -83,7 +88,9 @@ def new_init(self, authenticator: Authenticator = None):
8388
# Replacing BaseService's http.cookiejar.CookieJar as RequestsCookieJar supports update(CookieJar)
8489
self.jar = RequestsCookieJar(self.jar)
8590
self.authenticator.set_jar(self.jar) # Authenticators don't have access to cookie jars by default
86-
91+
response_hooks = self.get_http_client().hooks['response']
92+
if _error_response_hook not in response_hooks:
93+
response_hooks.append(_error_response_hook)
8794

8895
old_set_service_url = CloudantV1.set_service_url
8996

@@ -121,10 +128,73 @@ def new_set_disable_ssl_verification(self, status: bool = False) -> None:
121128
if isinstance(self.authenticator, CouchDbSessionAuthenticator):
122129
self.authenticator.token_manager.set_disable_ssl_verification(status)
123130

131+
def _error_response_hook(response:Response, *args, **kwargs) -> Optional[Response]:
132+
# pylint: disable=W0613
133+
# unused args and kwargs required by requests event hook interface
134+
"""Function for augmenting error responses.
135+
Converts the Cloudant response to better match the
136+
standard error response formats including adding a
137+
trace ID and appending the Cloudant/CouchDB error
138+
reason to the message.
139+
140+
Follows the requests event hook pattern.
141+
142+
:param response: the requests Response object
143+
:type response: Response
144+
145+
:return: A new response object, defaults to the existing response
146+
:rtype: Response,optional
147+
"""
148+
# Only hook into error responses
149+
# Ignore HEAD request responses because there is no body to read
150+
if not response.ok and response.request.method != 'HEAD':
151+
content_type = response.headers.get('content-type')
152+
# If it isn't JSON don't mess with it!
153+
if content_type is not None and content_type.startswith('application/json'):
154+
try:
155+
error_json: dict = response.json()
156+
# Only augment if there isn't a trace or errors already
157+
send_augmented_response = False
158+
if 'trace' not in error_json:
159+
if 'errors' not in error_json:
160+
error = error_json.get('error')
161+
reason = error_json.get('reason')
162+
if error is not None:
163+
error_model: dict = {'code': error, 'message': f'{error}'}
164+
if reason:
165+
error_model['message'] += f': {reason}'
166+
error_json['errors'] = [error_model]
167+
send_augmented_response = True
168+
if 'errors' in error_json:
169+
trace = response.headers.get('x-couch-request-id')
170+
if trace is not None:
171+
# Augment trace if there was a value
172+
error_json['trace'] = trace
173+
send_augmented_response = True
174+
if send_augmented_response:
175+
# It'd be nice to just change content on response, but it's internal.
176+
# Instead copy the named attributes to a new Response and then set
177+
# the encoding and bytes of the modified error body.
178+
error_response = Response()
179+
error_response.status_code = response.status_code
180+
error_response.headers = response.headers
181+
error_response.url = response.url
182+
error_response.history = response.history
183+
error_response.reason = response.reason
184+
error_response.cookies = response.cookies
185+
error_response.elapsed = response.elapsed
186+
error_response.request = response.request
187+
error_response.encoding = 'utf-8'
188+
error_response.raw = BytesIO(dumps(error_json).encode('utf-8'))
189+
return error_response
190+
except JSONDecodeError:
191+
# If we couldn't read the JSON we just return the response as-is
192+
# so the exception can surface elsewhere.
193+
pass
194+
return response
124195

125196
old_prepare_request = CloudantV1.prepare_request
126197

127-
128198
def new_prepare_request(self,
129199
method: str,
130200
url: str,

0 commit comments

Comments
 (0)