diff --git a/src/rotator_library/core/__init__.py b/src/rotator_library/core/__init__.py index c88a75a5..aa405479 100644 --- a/src/rotator_library/core/__init__.py +++ b/src/rotator_library/core/__init__.py @@ -28,6 +28,7 @@ NoAvailableKeysError, PreRequestCallbackError, CredentialNeedsReauthError, + IFlowNoAPIKeyError, EmptyResponseError, TransientQuotaError, StreamedAPIError, @@ -59,6 +60,7 @@ "NoAvailableKeysError", "PreRequestCallbackError", "CredentialNeedsReauthError", + "IFlowNoAPIKeyError", "EmptyResponseError", "TransientQuotaError", "StreamedAPIError", diff --git a/src/rotator_library/core/errors.py b/src/rotator_library/core/errors.py index 5acd9fc7..8d0268e1 100644 --- a/src/rotator_library/core/errors.py +++ b/src/rotator_library/core/errors.py @@ -18,6 +18,7 @@ NoAvailableKeysError, PreRequestCallbackError, CredentialNeedsReauthError, + IFlowNoAPIKeyError, EmptyResponseError, TransientQuotaError, # Error classification @@ -67,6 +68,7 @@ def __init__(self, message: str, data=None): "NoAvailableKeysError", "PreRequestCallbackError", "CredentialNeedsReauthError", + "IFlowNoAPIKeyError", "EmptyResponseError", "TransientQuotaError", "StreamedAPIError", diff --git a/src/rotator_library/error_handler.py b/src/rotator_library/error_handler.py index 9fd252c7..8eeca2a4 100644 --- a/src/rotator_library/error_handler.py +++ b/src/rotator_library/error_handler.py @@ -159,6 +159,34 @@ def __init__(self, credential_path: str, message: str = ""): super().__init__(self.message) +class IFlowNoAPIKeyError(Exception): + """ + Raised when an iFlow account has not set up an API key yet. + + This occurs when using cookie-based authentication with a new iFlow account + that hasn't created an API key through the web interface. + + Unlike ValueError, this exception can be caught programmatically to: + - Trigger OAuth authentication (which auto-creates an API key) + - Provide clear guidance to the user + - Distinguish from other authentication failures + + Attributes: + message: Human-readable message with guidance on how to fix + account_identifier: Optional identifier for the account (e.g., email or name) + """ + + def __init__(self, message: str = "", account_identifier: str = ""): + self.account_identifier = account_identifier + self.message = message or ( + "This iFlow account has not set up an API key yet. " + "Please either:\n" + "1. Use OAuth authentication (run with --oauth flag) which will automatically create an API key, OR\n" + "2. Manually create an API key at https://platform.iflow.cn/ and try again" + ) + super().__init__(self.message) + + class EmptyResponseError(Exception): """ Raised when a provider returns an empty response after multiple retry attempts. diff --git a/src/rotator_library/providers/iflow_auth_base.py b/src/rotator_library/providers/iflow_auth_base.py index 0ac03208..cc386db8 100644 --- a/src/rotator_library/providers/iflow_auth_base.py +++ b/src/rotator_library/providers/iflow_auth_base.py @@ -29,7 +29,11 @@ from ..utils.headless_detection import is_headless_environment from ..utils.reauth_coordinator import get_reauth_coordinator from ..utils.resilient_io import safe_write_json -from ..error_handler import CredentialNeedsReauthError +from ..error_handler import ( + CredentialNeedsReauthError, + IFlowNoAPIKeyError, + mask_credential, +) lib_logger = logging.getLogger("rotator_library") @@ -676,6 +680,15 @@ async def _fetch_api_key_info_with_cookie(self, cookie: str) -> Dict[str, Any]: error_msg = result.get("message", "Unknown error") raise ValueError(f"Cookie authentication failed: {error_msg}") + # Check if data is explicitly None (null) - indicates no API key exists yet + if result.get("data") is None: + lib_logger.warning( + "iFlow API returned null data - account has no API key configured" + ) + bx_auth_value = extract_bx_auth(cookie) or "" + masked_identifier = f"BXAuth={mask_credential(bx_auth_value)};" + raise IFlowNoAPIKeyError(account_identifier=masked_identifier) + data = result.get("data") or {} # Handle case where apiKey is masked - use apiKeyMask if apiKey is empty