Skip to content

Commit 6a450af

Browse files
committed
Merge pull request Mirrowel#142 from Mirrowel/dev
Resolve conflicts in iFlow provider by preserving existing retry/context-failure handling while integrating PR Mirrowel#142 signed-header fallback and base URL failover logic.
2 parents 5da1e7e + 97e1449 commit 6a450af

2 files changed

Lines changed: 282 additions & 61 deletions

File tree

src/rotator_library/providers/iflow_auth_base.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
# Cookie-based authentication endpoint
4848
IFLOW_API_KEY_ENDPOINT = "https://platform.iflow.cn/api/openapi/apikey"
49+
IFLOW_DEFAULT_API_BASE = "https://apis.iflow.cn/v1"
4950

5051
# Client credentials provided by iFlow
5152
IFLOW_CLIENT_ID = "10009311001"
@@ -335,6 +336,32 @@ def __init__(self):
335336
self._refresh_interval_seconds: int = 30 # Delay between queue items
336337
self._refresh_max_retries: int = 3 # Attempts before kicked out
337338

339+
def get_api_base_candidates(self) -> List[str]:
340+
"""Return ordered iFlow API base candidates from environment variables."""
341+
candidates: List[str] = []
342+
343+
single_base = os.getenv("IFLOW_API_BASE", "").strip()
344+
if single_base:
345+
normalized = single_base.rstrip("/")
346+
if normalized:
347+
candidates.append(normalized)
348+
349+
base_list = os.getenv("IFLOW_API_BASES", "").strip()
350+
if base_list:
351+
for item in base_list.split(","):
352+
normalized = item.strip().rstrip("/")
353+
if normalized and normalized not in candidates:
354+
candidates.append(normalized)
355+
356+
if IFLOW_DEFAULT_API_BASE not in candidates:
357+
candidates.append(IFLOW_DEFAULT_API_BASE)
358+
359+
return candidates
360+
361+
def get_api_base(self) -> str:
362+
"""Return the primary iFlow API base URL."""
363+
return self.get_api_base_candidates()[0]
364+
338365
def _parse_env_credential_path(self, path: str) -> Optional[str]:
339366
"""
340367
Parse a virtual env:// path and return the credential index.
@@ -978,12 +1005,32 @@ async def _refresh_token(self, path: str, force: bool = False) -> Dict[str, Any]
9781005
if not force and cached_creds and not self._is_token_expired(cached_creds):
9791006
return cached_creds
9801007

981-
# [ROTATING TOKEN FIX] Always read fresh from disk before refresh.
982-
# iFlow may use rotating refresh tokens - each refresh could invalidate the previous token.
983-
# If we use a stale cached token, refresh will fail.
984-
# Reading fresh from disk ensures we have the latest token.
985-
await self._read_creds_from_file(path)
986-
creds_from_file = self._credentials_cache[path]
1008+
# For file-based credentials, read fresh from disk before refresh.
1009+
# For env-loaded credentials, refresh using cached/env values (no file IO).
1010+
creds_from_file: Optional[Dict[str, Any]] = None
1011+
if cached_creds and cached_creds.get("_proxy_metadata", {}).get(
1012+
"loaded_from_env"
1013+
):
1014+
creds_from_file = cached_creds
1015+
elif self._parse_env_credential_path(path) is not None:
1016+
credential_index = self._parse_env_credential_path(path)
1017+
env_creds = self._load_from_env(credential_index)
1018+
if env_creds:
1019+
self._credentials_cache[path] = env_creds
1020+
creds_from_file = env_creds
1021+
else:
1022+
raise ValueError(
1023+
f"No environment credentials found for iFlow path: {path}"
1024+
)
1025+
else:
1026+
# [ROTATING TOKEN FIX] Always read fresh from disk before refresh.
1027+
# iFlow may use rotating refresh tokens - each refresh could invalidate
1028+
# the previous token. Fresh disk read keeps us in sync.
1029+
await self._read_creds_from_file(path)
1030+
creds_from_file = self._credentials_cache[path]
1031+
1032+
if creds_from_file is None:
1033+
raise ValueError(f"No credentials available for iFlow refresh: {path}")
9871034

9881035
lib_logger.debug(f"Refreshing iFlow OAuth token for '{Path(path).name}'...")
9891036
refresh_token = creds_from_file.get("refresh_token")
@@ -1228,7 +1275,8 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
12281275
- API Key: credential_identifier is the API key string itself
12291276
"""
12301277
# Detect credential type
1231-
if os.path.isfile(credential_identifier):
1278+
credential_index = self._parse_env_credential_path(credential_identifier)
1279+
if credential_index is not None or os.path.isfile(credential_identifier):
12321280
creds = await self._load_credentials(credential_identifier)
12331281

12341282
# Check if this is a cookie-based credential
@@ -1262,7 +1310,7 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
12621310
lib_logger.debug("Using direct API key for iFlow")
12631311
api_key = credential_identifier
12641312

1265-
base_url = "https://apis.iflow.cn/v1"
1313+
base_url = self.get_api_base()
12661314
return base_url, api_key
12671315

12681316
async def proactively_refresh(self, credential_identifier: str):

0 commit comments

Comments
 (0)