77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from yuxi.channel.errors import ErrorSeverity
|
||
|
|
from yuxi.channel.protocols import ClassifiedError, ErrorHandlingProtocol
|
||
|
|
|
||
|
|
MAX_RETRIES = 3
|
||
|
|
|
||
|
|
|
||
|
|
def classify_error(
|
||
|
|
status_code: int | None, response_body: dict | str | None
|
||
|
|
) -> tuple[ErrorSeverity, str, float | None]:
|
||
|
|
description = ""
|
||
|
|
if isinstance(response_body, dict):
|
||
|
|
errors = response_body.get("errorMessages", response_body.get("errors", {}))
|
||
|
|
if isinstance(errors, list) and errors:
|
||
|
|
description = errors[0]
|
||
|
|
elif isinstance(errors, dict) and errors:
|
||
|
|
description = list(errors.values())[0]
|
||
|
|
elif isinstance(response_body, str):
|
||
|
|
description = response_body
|
||
|
|
|
||
|
|
if status_code == 429:
|
||
|
|
return ErrorSeverity.RATE_LIMITED, description, 30.0
|
||
|
|
|
||
|
|
if status_code == 401:
|
||
|
|
return ErrorSeverity.FORBIDDEN, description, None
|
||
|
|
|
||
|
|
if status_code == 403:
|
||
|
|
return ErrorSeverity.FORBIDDEN, description, None
|
||
|
|
|
||
|
|
if status_code == 404:
|
||
|
|
return ErrorSeverity.FATAL, description, None
|
||
|
|
|
||
|
|
if status_code == 400:
|
||
|
|
return ErrorSeverity.FATAL, description, None
|
||
|
|
|
||
|
|
if status_code and status_code >= 500:
|
||
|
|
return ErrorSeverity.RETRYABLE, description, None
|
||
|
|
|
||
|
|
if status_code is None:
|
||
|
|
return ErrorSeverity.NETWORK, description, None
|
||
|
|
|
||
|
|
return ErrorSeverity.FATAL, description, None
|
||
|
|
|
||
|
|
|
||
|
|
class JiraErrorHandler(ErrorHandlingProtocol):
|
||
|
|
def classify_error(self, error: BaseException) -> ClassifiedError:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
if isinstance(error, httpx.HTTPStatusError):
|
||
|
|
status_code = error.response.status_code
|
||
|
|
body = error.response.json() if error.response.content else {}
|
||
|
|
severity, desc, retry_after = classify_error(status_code, body)
|
||
|
|
return ClassifiedError(
|
||
|
|
severity=severity,
|
||
|
|
retry_after_ms=int(retry_after * 1000) if retry_after else 0,
|
||
|
|
original_error=error,
|
||
|
|
error_message=desc,
|
||
|
|
)
|
||
|
|
return ClassifiedError(
|
||
|
|
severity=ErrorSeverity.NETWORK,
|
||
|
|
retry_after_ms=0,
|
||
|
|
original_error=error,
|
||
|
|
error_message=str(error),
|
||
|
|
)
|
||
|
|
|
||
|
|
def is_retryable(self, error: BaseException) -> bool:
|
||
|
|
classified = self.classify_error(error)
|
||
|
|
return classified.severity in (ErrorSeverity.RETRYABLE, ErrorSeverity.RATE_LIMITED, ErrorSeverity.NETWORK)
|
||
|
|
|
||
|
|
def should_backoff(self, error: BaseException, attempt: int) -> int:
|
||
|
|
if not self.is_retryable(error):
|
||
|
|
return 0
|
||
|
|
if attempt >= MAX_RETRIES:
|
||
|
|
return 0
|
||
|
|
return min(2**attempt * 1000, 10000)
|