-
-
Notifications
You must be signed in to change notification settings - Fork 73
Harden iFlow proxy requests to match CLI behavior #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
redzrush101
wants to merge
6
commits into
Mirrowel:dev
Choose a base branch
from
redzrush101:iflow-cli-spoof-port
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
97e1449
Harden iFlow proxy requests to match CLI behavior
407032e
Complete iFlow CLI parity and restore thinking behavior
b04c22e
Reduce iFlow proxy stream logging overhead
14dc122
fix(error-handler): add RemoteProtocolError to api_connection classif…
MasuRii bf4dace
fix(iflow): add connection error handling with retry logic in stream_…
MasuRii bc58e34
fix(iflow): add silent context failure detection for empty 200 responses
MasuRii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,8 @@ | |
|
|
||
| # Cookie-based authentication endpoint | ||
| IFLOW_API_KEY_ENDPOINT = "https://platform.iflow.cn/api/openapi/apikey" | ||
| IFLOW_DEFAULT_API_BASE = "https://apis.iflow.cn/v1" | ||
| IFLOW_CLI_USER_AGENT = "iFlow-Cli" | ||
|
|
||
| # Client credentials provided by iFlow | ||
| IFLOW_CLIENT_ID = "10009311001" | ||
|
|
@@ -331,6 +333,32 @@ def __init__(self): | |
| self._refresh_interval_seconds: int = 30 # Delay between queue items | ||
| self._refresh_max_retries: int = 3 # Attempts before kicked out | ||
|
|
||
| def get_api_base_candidates(self) -> List[str]: | ||
| """Return ordered iFlow API base candidates from environment variables.""" | ||
| candidates: List[str] = [] | ||
|
|
||
| single_base = os.getenv("IFLOW_API_BASE", "").strip() | ||
| if single_base: | ||
| normalized = single_base.rstrip("/") | ||
| if normalized: | ||
| candidates.append(normalized) | ||
|
|
||
| base_list = os.getenv("IFLOW_API_BASES", "").strip() | ||
| if base_list: | ||
| for item in base_list.split(","): | ||
| normalized = item.strip().rstrip("/") | ||
| if normalized and normalized not in candidates: | ||
| candidates.append(normalized) | ||
|
|
||
| if IFLOW_DEFAULT_API_BASE not in candidates: | ||
| candidates.append(IFLOW_DEFAULT_API_BASE) | ||
|
|
||
| return candidates | ||
|
|
||
| def get_api_base(self) -> str: | ||
| """Return the primary iFlow API base URL.""" | ||
| return self.get_api_base_candidates()[0] | ||
|
|
||
| def _parse_env_credential_path(self, path: str) -> Optional[str]: | ||
| """ | ||
| Parse a virtual env:// path and return the credential index. | ||
|
|
@@ -611,11 +639,21 @@ async def _fetch_user_info(self, access_token: str) -> Dict[str, Any]: | |
| if not access_token or not access_token.strip(): | ||
| raise ValueError("Access token is empty") | ||
|
|
||
| url = f"{IFLOW_USER_INFO_ENDPOINT}?accessToken={access_token}" | ||
| headers = {"Accept": "application/json"} | ||
| headers = { | ||
| "Accept": "*/*", | ||
| "accessToken": access_token, | ||
| "User-Agent": "node", | ||
| "Accept-Language": "*", | ||
| "Sec-Fetch-Mode": "cors", | ||
| "Accept-Encoding": "br, gzip, deflate", | ||
| } | ||
|
|
||
| async with httpx.AsyncClient(timeout=30.0) as client: | ||
| response = await client.get(url, headers=headers) | ||
| response = await client.get( | ||
| IFLOW_USER_INFO_ENDPOINT, | ||
| headers=headers, | ||
| params={"accessToken": access_token}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| ) | ||
| response.raise_for_status() | ||
| result = response.json() | ||
|
|
||
|
|
@@ -965,12 +1003,32 @@ async def _refresh_token(self, path: str, force: bool = False) -> Dict[str, Any] | |
| if not force and cached_creds and not self._is_token_expired(cached_creds): | ||
| return cached_creds | ||
|
|
||
| # [ROTATING TOKEN FIX] Always read fresh from disk before refresh. | ||
| # iFlow may use rotating refresh tokens - each refresh could invalidate the previous token. | ||
| # If we use a stale cached token, refresh will fail. | ||
| # Reading fresh from disk ensures we have the latest token. | ||
| await self._read_creds_from_file(path) | ||
| creds_from_file = self._credentials_cache[path] | ||
| # For file-based credentials, read fresh from disk before refresh. | ||
| # For env-loaded credentials, refresh using cached/env values (no file IO). | ||
| creds_from_file: Optional[Dict[str, Any]] = None | ||
| if cached_creds and cached_creds.get("_proxy_metadata", {}).get( | ||
| "loaded_from_env" | ||
| ): | ||
| creds_from_file = cached_creds | ||
| elif self._parse_env_credential_path(path) is not None: | ||
| credential_index = self._parse_env_credential_path(path) | ||
| env_creds = self._load_from_env(credential_index) | ||
| if env_creds: | ||
| self._credentials_cache[path] = env_creds | ||
| creds_from_file = env_creds | ||
| else: | ||
| raise ValueError( | ||
| f"No environment credentials found for iFlow path: {path}" | ||
| ) | ||
| else: | ||
| # [ROTATING TOKEN FIX] Always read fresh from disk before refresh. | ||
| # iFlow may use rotating refresh tokens - each refresh could invalidate | ||
| # the previous token. Fresh disk read keeps us in sync. | ||
| await self._read_creds_from_file(path) | ||
| creds_from_file = self._credentials_cache[path] | ||
|
|
||
| if creds_from_file is None: | ||
| raise ValueError(f"No credentials available for iFlow refresh: {path}") | ||
|
|
||
| lib_logger.debug(f"Refreshing iFlow OAuth token for '{Path(path).name}'...") | ||
| refresh_token = creds_from_file.get("refresh_token") | ||
|
|
@@ -1215,7 +1273,8 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]: | |
| - API Key: credential_identifier is the API key string itself | ||
| """ | ||
| # Detect credential type | ||
| if os.path.isfile(credential_identifier): | ||
| credential_index = self._parse_env_credential_path(credential_identifier) | ||
| if credential_index is not None or os.path.isfile(credential_identifier): | ||
| creds = await self._load_credentials(credential_identifier) | ||
|
|
||
| # Check if this is a cookie-based credential | ||
|
|
@@ -1243,13 +1302,30 @@ async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]: | |
|
|
||
| api_key = creds.get("api_key") | ||
| if not api_key: | ||
| raise ValueError("Missing api_key in iFlow OAuth credentials") | ||
| access_token = creds.get("access_token", "") | ||
| if access_token: | ||
| try: | ||
| user_info = await self._fetch_user_info(access_token) | ||
| api_key = user_info.get("api_key", "") | ||
| if api_key: | ||
| creds["api_key"] = api_key | ||
| if credential_index is None: | ||
| await self._save_credentials( | ||
| credential_identifier, | ||
| creds, | ||
| ) | ||
| except Exception as e: | ||
| lib_logger.warning( | ||
| f"Failed to recover iFlow api_key from OAuth user info: {e}" | ||
| ) | ||
| if not api_key: | ||
| raise ValueError("Missing api_key in iFlow OAuth credentials") | ||
| else: | ||
| # Direct API key: use as-is | ||
| lib_logger.debug("Using direct API key for iFlow") | ||
| api_key = credential_identifier | ||
|
|
||
| base_url = "https://apis.iflow.cn/v1" | ||
| base_url = self.get_api_base() | ||
| return base_url, api_key | ||
|
|
||
| async def proactively_refresh(self, credential_identifier: str): | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function
_inject_iflow_metadata_from_incoming_headersmodifiesrequest_datain-place. Consider documenting this side effect in the docstring to make the mutation explicit.