Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/rotator_library/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
NoAvailableKeysError,
PreRequestCallbackError,
CredentialNeedsReauthError,
IFlowNoAPIKeyError,
EmptyResponseError,
TransientQuotaError,
StreamedAPIError,
Expand Down Expand Up @@ -59,6 +60,7 @@
"NoAvailableKeysError",
"PreRequestCallbackError",
"CredentialNeedsReauthError",
"IFlowNoAPIKeyError",
"EmptyResponseError",
"TransientQuotaError",
"StreamedAPIError",
Expand Down
2 changes: 2 additions & 0 deletions src/rotator_library/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
NoAvailableKeysError,
PreRequestCallbackError,
CredentialNeedsReauthError,
IFlowNoAPIKeyError,
EmptyResponseError,
TransientQuotaError,
# Error classification
Expand Down Expand Up @@ -67,6 +68,7 @@ def __init__(self, message: str, data=None):
"NoAvailableKeysError",
"PreRequestCallbackError",
"CredentialNeedsReauthError",
"IFlowNoAPIKeyError",
"EmptyResponseError",
"TransientQuotaError",
"StreamedAPIError",
Expand Down
28 changes: 28 additions & 0 deletions src/rotator_library/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 14 additions & 1 deletion src/rotator_library/providers/iflow_auth_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down