From 0c46c05e1fc9753ffd3d971fb08b105bbdc9f777 Mon Sep 17 00:00:00 2001 From: MasuRii Date: Tue, 17 Feb 2026 16:24:46 +0800 Subject: [PATCH 1/2] fix(iflow): handle null data for cookie auth on new accounts When a new iFlow account (that hasn't completed OAuth) uses cookie-based authentication, the API returns null data because no API key exists yet. Changes: - Add explicit null data detection in _fetch_api_key_info_with_cookie() - Create IFlowNoAPIKeyError custom exception with actionable guidance - Provide clear instructions: use OAuth (auto-creates API key) or manually create at platform.iflow.cn This fixes authentication failures for new accounts while maintaining backward compatibility for existing accounts with valid API keys. --- src/rotator_library/error_handler.py | 28 +++++++++++++++++++ .../providers/iflow_auth_base.py | 9 +++++- 2 files changed, 36 insertions(+), 1 deletion(-) 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..2f035f2e 100644 --- a/src/rotator_library/providers/iflow_auth_base.py +++ b/src/rotator_library/providers/iflow_auth_base.py @@ -29,7 +29,7 @@ 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 lib_logger = logging.getLogger("rotator_library") @@ -676,6 +676,13 @@ 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" + ) + raise IFlowNoAPIKeyError() + data = result.get("data") or {} # Handle case where apiKey is masked - use apiKeyMask if apiKey is empty From 9c7484706329fbf642a0c20b103c9e819c810ca1 Mon Sep 17 00:00:00 2001 From: MasuRii Date: Thu, 19 Feb 2026 18:44:40 +0800 Subject: [PATCH 2/2] fix(iflow): export IFlowNoAPIKeyError and add account context Export IFlowNoAPIKeyError through core error re-exports for consistency with other error classes in the module hierarchy. Pass masked BXAuth identifier when raising IFlowNoAPIKeyError to provide multi-account clarity in error messages and logs. --- src/rotator_library/core/__init__.py | 2 ++ src/rotator_library/core/errors.py | 2 ++ src/rotator_library/providers/iflow_auth_base.py | 10 ++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) 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/providers/iflow_auth_base.py b/src/rotator_library/providers/iflow_auth_base.py index 2f035f2e..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, IFlowNoAPIKeyError +from ..error_handler import ( + CredentialNeedsReauthError, + IFlowNoAPIKeyError, + mask_credential, +) lib_logger = logging.getLogger("rotator_library") @@ -681,7 +685,9 @@ async def _fetch_api_key_info_with_cookie(self, cookie: str) -> Dict[str, Any]: lib_logger.warning( "iFlow API returned null data - account has no API key configured" ) - raise IFlowNoAPIKeyError() + 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 {}