Skip to content

Commit 65ed26f

Browse files
author
Baz
authored
feat: (Airbyte Python CDK) - extend the error_message info for the ResponseAction.FAIL request scenario (#87)
1 parent 5f0831e commit 65ed26f

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ Installing all extras is required to run the full suite of unit tests.
9393

9494
To see all available scripts, run `poetry run poe`.
9595

96+
#### Formatting the code
97+
98+
- Iterate on the CDK code locally
99+
- Run `poetry run ruff format` to format your changes.
100+
101+
To see all available `ruff` options, run `poetry run ruff`.
102+
96103
##### Autogenerated files
97104

98105
Low-code CDK models are generated from `sources/declarative/declarative_component_schema.yaml`. If

airbyte_cdk/sources/streams/http/http_client.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
rate_limit_default_backoff_handler,
4646
user_defined_backoff_handler,
4747
)
48+
from airbyte_cdk.sources.utils.types import JsonType
49+
from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets
4850
from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH
4951
from airbyte_cdk.utils.stream_status_utils import (
5052
as_airbyte_message as stream_status_as_airbyte_message,
@@ -334,6 +336,29 @@ def _send(
334336

335337
return response # type: ignore # will either return a valid response of type requests.Response or raise an exception
336338

339+
def _get_response_body(self, response: requests.Response) -> Optional[JsonType]:
340+
"""
341+
Extracts and returns the body of an HTTP response.
342+
343+
This method attempts to parse the response body as JSON. If the response
344+
body is not valid JSON, it falls back to decoding the response content
345+
as a UTF-8 string. If both attempts fail, it returns None.
346+
347+
Args:
348+
response (requests.Response): The HTTP response object.
349+
350+
Returns:
351+
Optional[JsonType]: The parsed JSON object as a string, the decoded
352+
response content as a string, or None if both parsing attempts fail.
353+
"""
354+
try:
355+
return str(response.json())
356+
except requests.exceptions.JSONDecodeError:
357+
try:
358+
return response.content.decode("utf-8")
359+
except Exception:
360+
return "The Content of the Response couldn't be decoded."
361+
337362
def _handle_error_resolution(
338363
self,
339364
response: Optional[requests.Response],
@@ -362,12 +387,18 @@ def _handle_error_resolution(
362387

363388
if error_resolution.response_action == ResponseAction.FAIL:
364389
if response is not None:
365-
error_message = f"'{request.method}' request to '{request.url}' failed with status code '{response.status_code}' and error message '{self._error_message_parser.parse_response_error_message(response)}'"
390+
filtered_response_message = filter_secrets(
391+
f"Request (body): '{str(request.body)}'. Response (body): '{self._get_response_body(response)}'. Response (headers): '{response.headers}'."
392+
)
393+
error_message = f"'{request.method}' request to '{request.url}' failed with status code '{response.status_code}' and error message: '{self._error_message_parser.parse_response_error_message(response)}'. {filtered_response_message}"
366394
else:
367395
error_message = (
368396
f"'{request.method}' request to '{request.url}' failed with exception: '{exc}'"
369397
)
370398

399+
# ensure the exception message is emitted before raised
400+
self._logger.error(error_message)
401+
371402
raise MessageRepresentationAirbyteTracedErrors(
372403
internal_message=error_message,
373404
message=error_resolution.error_message or error_message,

unit_tests/sources/streams/http/test_http.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
44

5-
65
import json
76
import logging
87
from http import HTTPStatus
@@ -29,6 +28,7 @@
2928
)
3029
from airbyte_cdk.sources.streams.http.http_client import MessageRepresentationAirbyteTracedErrors
3130
from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator
31+
from airbyte_cdk.utils.airbyte_secrets_utils import update_secrets
3232

3333

3434
class StubBasicReadHttpStream(HttpStream):
@@ -230,6 +230,43 @@ def test_4xx_error_codes_http_stream(mocker, http_code):
230230
list(stream.read_records(SyncMode.full_refresh))
231231

232232

233+
@pytest.mark.parametrize("http_code", [400, 401, 403])
234+
def test_error_codes_http_stream_error_resolution_with_response_secrets_filtered(mocker, http_code):
235+
stream = StubCustomBackoffHttpStream()
236+
237+
# expected assertion values
238+
expected_header_secret_replaced = "'authorisation_header': '__****__'"
239+
expected_content_str_secret_replaced = "this str contains **** secret"
240+
241+
# mocking the response
242+
res = requests.Response()
243+
res.status_code = http_code
244+
res._content = (
245+
b'{"error": "test error message", "secret_info": "this str contains SECRET_VALUE secret"}'
246+
)
247+
res.headers = {
248+
# simple non-secret header
249+
"regular_header": "some_header_value",
250+
# secret header
251+
"authorisation_header": "__SECRET_X_VALUE__",
252+
}
253+
254+
# updating secrets to be filtered
255+
update_secrets(["SECRET_X_VALUE", "SECRET_VALUE"])
256+
257+
# patch the `send` > response
258+
mocker.patch.object(requests.Session, "send", return_value=res)
259+
260+
# proceed
261+
with pytest.raises(MessageRepresentationAirbyteTracedErrors) as err:
262+
list(stream.read_records(SyncMode.full_refresh))
263+
264+
# we expect the header secrets are obscured
265+
assert expected_header_secret_replaced in str(err._excinfo)
266+
# we expect the response body values (any of them) are obscured
267+
assert expected_content_str_secret_replaced in str(err._excinfo)
268+
269+
233270
class AutoFailFalseHttpStream(StubBasicReadHttpStream):
234271
raise_on_http_errors = False
235272
max_retries = 3

0 commit comments

Comments
 (0)