-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support custom error messaging for error response + retryable errors #18204
Changes from 1 commit
71e8acb
fde72a5
b6c8d15
a106794
cf30816
bc0dfba
ccfa982
6123f01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,10 @@ | |
from typing import Any, Mapping, Optional, Set, Union | ||
|
||
import requests | ||
from airbyte_cdk.sources.declarative.interpolation import InterpolatedString | ||
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean | ||
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_action import ResponseAction | ||
from airbyte_cdk.sources.declarative.types import Config | ||
from airbyte_cdk.sources.streams.http.http import HttpStream | ||
from dataclasses_jsonschema import JsonSchemaMixin | ||
|
||
|
@@ -22,23 +24,27 @@ class HttpResponseFilter(JsonSchemaMixin): | |
http_codes (Set[int]): http code of matching requests | ||
error_message_contains (str): error substring of matching requests | ||
predicate (str): predicate to apply to determine if a request is matching | ||
error_message (Union[InterpolatedString, str): error message to display if the response matches the filter | ||
""" | ||
|
||
TOO_MANY_REQUESTS_ERRORS = {429} | ||
DEFAULT_RETRIABLE_ERRORS = set([x for x in range(500, 600)]).union(TOO_MANY_REQUESTS_ERRORS) | ||
|
||
action: Union[ResponseAction, str] | ||
config: Config | ||
options: InitVar[Mapping[str, Any]] | ||
http_codes: Set[int] = None | ||
error_message_contains: str = None | ||
predicate: Union[InterpolatedBoolean, str] = "" | ||
error_message: Union[InterpolatedString, str] = "" | ||
|
||
def __post_init__(self, options: Mapping[str, Any]): | ||
if isinstance(self.action, str): | ||
self.action = ResponseAction[self.action] | ||
self.http_codes = self.http_codes or set() | ||
if isinstance(self.predicate, str): | ||
self.predicate = InterpolatedBoolean(condition=self.predicate, options=options) | ||
self.error_message = InterpolatedString.create(string_or_interpolated=self.error_message, options=options) | ||
|
||
def matches(self, response: requests.Response) -> Optional[ResponseAction]: | ||
""" | ||
|
@@ -55,6 +61,16 @@ def matches(self, response: requests.Response) -> Optional[ResponseAction]: | |
else: | ||
return None | ||
|
||
def create_error_message(self, response: requests.Response) -> str: | ||
""" | ||
Construct an error message based on the specified message template of the filter. | ||
:param response: The HTTP response which can be used during interpolation | ||
:return: The evaluated error message string to be emitted | ||
""" | ||
if self.error_message: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice yeah this can basically just get condensed into a one liner |
||
return self.error_message.eval(self.config, response=response.json(), headers=response.headers) | ||
return "" | ||
|
||
def _response_matches_predicate(self, response: requests.Response) -> bool: | ||
return self.predicate and self.predicate.eval(None, response=response.json(), headers=response.headers) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,20 +9,22 @@ | |
|
||
class ResponseStatus: | ||
""" | ||
ResponseAction amended with backoff time if a action is RETRY | ||
ResponseAction amended with backoff time if an action is RETRY | ||
""" | ||
|
||
def __init__(self, response_action: Union[ResponseAction, str], retry_in: Optional[float] = None): | ||
def __init__(self, response_action: Union[ResponseAction, str], retry_in: Optional[float] = None, error_message: str = ""): | ||
""" | ||
:param response_action: response action to execute | ||
:param retry_in: backoff time (if action is RETRY) | ||
:param error_message: the error to be displayed back to the customer | ||
""" | ||
if isinstance(response_action, str): | ||
response_action = ResponseAction[response_action] | ||
if retry_in and response_action != ResponseAction.RETRY: | ||
raise ValueError(f"Unexpected backoff time ({retry_in} for non-retryable response action {response_action}") | ||
self._retry_in = retry_in | ||
self._action = response_action | ||
self._error_message = error_message | ||
|
||
@property | ||
def action(self): | ||
|
@@ -34,6 +36,11 @@ def retry_in(self) -> Optional[float]: | |
"""How long to backoff before retrying a response. None if no wait required.""" | ||
return self._retry_in | ||
|
||
@property | ||
def error_message(self) -> Optional[str]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this always return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yeah good point will just make this a string |
||
"""The message to be displayed when an error response is received""" | ||
return self._error_message | ||
|
||
@classmethod | ||
def retry(cls, retry_in: Optional[float]) -> "ResponseStatus": | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -243,6 +243,15 @@ def backoff_time(self, response: requests.Response) -> Optional[float]: | |
""" | ||
return None | ||
|
||
def error_message(self, response: requests.Response) -> str: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't love the idea of a new method to the CDK to support a low-code use case, but it could still be useful for other connectors that need to override retryable errors There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interfacing with HttpStream is always tricky. I'm in favor of those refactors so long as the default behavior does not change so we don't impact existing connectors. |
||
""" | ||
Override this method to specify a custom error message which can incorporate the HTTP response received | ||
|
||
:param response: The incoming HTTP response from the partner API | ||
:return: | ||
""" | ||
return "" | ||
|
||
def _create_prepared_request( | ||
self, | ||
path: str, | ||
|
@@ -296,10 +305,13 @@ def _send(self, request: requests.PreparedRequest, request_kwargs: Mapping[str, | |
) | ||
if self.should_retry(response): | ||
custom_backoff_time = self.backoff_time(response) | ||
error_message = self.error_message(response) | ||
if custom_backoff_time: | ||
raise UserDefinedBackoffException(backoff=custom_backoff_time, request=request, response=response) | ||
raise UserDefinedBackoffException( | ||
backoff=custom_backoff_time, request=request, response=response, error_message=error_message | ||
) | ||
else: | ||
raise DefaultBackoffException(request=request, response=response) | ||
raise DefaultBackoffException(request=request, response=response, error_message=error_message) | ||
elif self.raise_on_http_errors: | ||
# Raise any HTTP exceptions that happened in case there were unexpected ones | ||
try: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed this method because in
simple_retriever.parse_response()
we're not just getting the retryability, we're interpreting what the response status which is probably a more accurate name