ForcePilot/backend/package/yuxi/channel/extensions/jira/errors.py

77 lines
2.6 KiB
Python
Raw Normal View History

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)