|
1 | 1 | # coding: utf-8
|
2 | 2 |
|
3 |
| -# © Copyright IBM Corporation 2020, 2022. |
| 3 | +# © Copyright IBM Corporation 2020, 2024. |
4 | 4 | #
|
5 | 5 | # Licensed under the Apache License, Version 2.0 (the "License");
|
6 | 6 | # you may not use this file except in compliance with the License.
|
|
15 | 15 | # limitations under the License.
|
16 | 16 | """
|
17 | 17 | Module to patch sdk core base service for session authentication
|
| 18 | +and other helpful features. |
18 | 19 | """
|
19 | 20 | from collections import namedtuple
|
20 | 21 | from typing import Dict, Optional, Union, Tuple, List
|
21 | 22 | from urllib.parse import urlsplit, unquote
|
| 23 | +from json import dumps |
| 24 | +from json.decoder import JSONDecodeError |
| 25 | +from io import BytesIO |
22 | 26 |
|
23 | 27 | from ibm_cloud_sdk_core.authenticators import Authenticator
|
| 28 | +from requests import Response |
24 | 29 | from requests.cookies import RequestsCookieJar
|
25 | 30 |
|
26 | 31 | from .common import get_sdk_headers
|
@@ -83,7 +88,9 @@ def new_init(self, authenticator: Authenticator = None):
|
83 | 88 | # Replacing BaseService's http.cookiejar.CookieJar as RequestsCookieJar supports update(CookieJar)
|
84 | 89 | self.jar = RequestsCookieJar(self.jar)
|
85 | 90 | 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) |
87 | 94 |
|
88 | 95 | old_set_service_url = CloudantV1.set_service_url
|
89 | 96 |
|
@@ -121,10 +128,73 @@ def new_set_disable_ssl_verification(self, status: bool = False) -> None:
|
121 | 128 | if isinstance(self.authenticator, CouchDbSessionAuthenticator):
|
122 | 129 | self.authenticator.token_manager.set_disable_ssl_verification(status)
|
123 | 130 |
|
| 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 |
124 | 195 |
|
125 | 196 | old_prepare_request = CloudantV1.prepare_request
|
126 | 197 |
|
127 |
| - |
128 | 198 | def new_prepare_request(self,
|
129 | 199 | method: str,
|
130 | 200 | url: str,
|
|
0 commit comments