Handle invalidated token#336
Open
iLLiCiTiT wants to merge 23 commits into
Open
Conversation
BigRoy
reviewed
Jun 8, 2026
There was a problem hiding this comment.
Pull request overview
This PR adds token invalidation handling to prevent repeated requests after the server returns HTTP 401, and refactors token/user validation by introducing a UserInfo return type and a TokenInfo dataclass to centralize token state in ServerAPI.
Changes:
- Introduces
get_user_info_by_token()(returning aUserInfodataclass) and adds a deprecation wrapper for positionaltimeoutusage in several utils functions. - Refactors
ServerAPIto track token state in aTokenInfodataclass and to invalidate/short-circuit requests after a401. - Updates global API wrapper to reference the new token storage location (
_token_info.token).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
ayon_api/utils.py |
Adds UserInfo + get_user_info_by_token() and introduces a timeout positional-arg deprecation decorator. |
ayon_api/server_api.py |
Refactors token tracking into TokenInfo, adds 401-driven invalidation + request short-circuiting, and adjusts login/logout/token validation flow. |
ayon_api/_api.py |
Updates global wrapper logic to use _token_info.token instead of the old _access_token. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
768
to
+797
| def validate_token(self) -> bool: | ||
| if self._token_info.token is None: | ||
| self._token_info.is_valid = False | ||
| self.close_session() | ||
| return False | ||
|
|
||
| try: | ||
| self._token_validation_started = True | ||
| # TODO add other possible validations | ||
| # - existence of 'user' key in info | ||
| # - validate that 'site_id' is in 'sites' in info | ||
| self.get_info() | ||
| self.get_user() | ||
| self._token_is_valid = True | ||
| self._get_server_info() | ||
|
|
||
| except UnauthorizedError: | ||
| self._token_is_valid = False | ||
| user_info = get_user_info_by_token( | ||
| self.base_url, | ||
| self._token_info.token, | ||
| verify=self._ssl_verify, | ||
| cert=self._cert | ||
| ) | ||
| self._token_info.is_valid = user_info.is_valid | ||
| is_service = None | ||
| if user_info.is_valid: | ||
| is_service = user_info.is_service | ||
| self._token_info.is_service = is_service | ||
|
|
||
| except Exception: | ||
| self._token_info.is_valid = False | ||
| self.close_session() | ||
| self.log.error("Failed to validate token.", exc_info=True) | ||
|
|
||
| finally: | ||
| self._token_validation_started = False | ||
| return self._token_is_valid | ||
| return self._token_info.is_valid |
Comment on lines
+1497
to
+1620
| def _do_rest_request( | ||
| self, | ||
| function: Any, | ||
| url: str, | ||
| *, | ||
| handle_invalid_token: bool = True, | ||
| **kwargs | ||
| ): | ||
| kwargs.setdefault("timeout", self.timeout) | ||
| max_retries = kwargs.get("max_retries", self.max_retries) | ||
| if max_retries < 1: | ||
| max_retries = 1 | ||
|
|
||
| if handle_invalid_token and self._token_info.is_valid is False: | ||
| # Return a fake error response if the token is known to be invalid. | ||
| # Added to prevent DDOS attack on server when many requests | ||
| # with invalid token are send. It is better to return error | ||
| # immediately without trying to send a request to server. | ||
| # NOTE maybe store last know response data and re-use it? | ||
| detail = "Access token is missing" | ||
| if self._token_info.is_service: | ||
| detail = "Invalid API key" | ||
| new_response = RestApiResponse( | ||
| None, | ||
| {"code": 401, "detail": detail} | ||
| ) | ||
| new_response.status = 401 | ||
| return new_response | ||
|
|
||
| if self._session is None: | ||
| # Validate token if was not yet validated | ||
| if ( | ||
| handle_invalid_token | ||
| and self._token_info.is_valid is None | ||
| ): | ||
| self.validate_token() | ||
|
|
||
| if "headers" not in kwargs: | ||
| kwargs["headers"] = self.get_headers() | ||
|
|
||
| if isinstance(function, RequestType): | ||
| function = self._base_functions_mapping[function] | ||
|
|
||
| elif isinstance(function, RequestType): | ||
| function = self._session_functions_mapping[function] | ||
|
|
||
| response = None | ||
| new_response = None | ||
| for retry_idx in reversed(range(max_retries)): | ||
| try: | ||
| response = function(url, **kwargs) | ||
|
|
||
| # Usually these mean, try later. | ||
| # 502: returned by the proxy: nginx | ||
| # 503: returned by the server: if no capacity | ||
| if response.status_code in {502, 503}: | ||
| new_response = RestApiResponse(response) | ||
| self.log.warning( | ||
| "Server returned %s status code." | ||
| " Retrying with longer delay...", | ||
| response.status_code | ||
| ) | ||
| if retry_idx != 0: | ||
| time.sleep(2) | ||
| continue | ||
| break | ||
|
|
||
| except ConnectionRefusedError: | ||
| if retry_idx == 0: | ||
| self.log.warning( | ||
| "Connection error happened.", exc_info=True | ||
| ) | ||
|
|
||
| # Server may be restarting | ||
| new_response = RestApiResponse( | ||
| None, | ||
| { | ||
| "detail": ( | ||
| "Unable to connect the server. Connection refused" | ||
| ) | ||
| } | ||
| ) | ||
|
|
||
| except requests.exceptions.Timeout: | ||
| # Connection timed out | ||
| new_response = RestApiResponse( | ||
| None, | ||
| {"detail": "Connection timed out."} | ||
| ) | ||
|
|
||
| except requests.exceptions.ConnectionError: | ||
| # Log warning only on last attempt | ||
| if retry_idx == 0: | ||
| self.log.warning( | ||
| "Connection error happened.", exc_info=True | ||
| ) | ||
|
|
||
| new_response = RestApiResponse( | ||
| None, | ||
| { | ||
| "detail": ( | ||
| "Unable to connect the server. Connection error" | ||
| ) | ||
| } | ||
| ) | ||
|
|
||
| if retry_idx != 0: | ||
| time.sleep(0.1) | ||
|
|
||
| if new_response is not None: | ||
| return new_response | ||
|
|
||
| new_response = RestApiResponse(response) | ||
| if ( | ||
| handle_invalid_token | ||
| and new_response.status_code == 401 | ||
| and self._token_info.is_valid | ||
| ): | ||
| self._token_info.is_valid = False | ||
| self.close_session() | ||
|
|
||
| self.log.debug(f"Response {str(new_response)}") | ||
| return new_response | ||
|
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Changelog Description
Handle cases when
401status is returned from a server on a request and prematurelly stop.Additional review information
If server returns response with
401status the token is being invalidated and next requests to server are stopped. There is a room for enhancement as there might be methods that should allow to ignore invalid token, I will track as much as possible, but it is NOT possible to catch all of them, e.g. public files in addons technically do not require token, but we don't know that ahead.Few major changes were required to achieve the goal with a persistency. The token validation within the same class become a mess of state handling. To avoid the issues I've implemented new function
get_user_info_by_tokenin utils that does validate the token and returns more information we need inServerAPI. Token information used as internal information ofServerAPIare now wrapped in a dataclass to help organize it a little. Few code optimizations were possible with that approach.In ideal case we should add an event system so it is possible to trigger an event when it happens and the process using ayon api can handle it. I would rather do that when we do add websockets connection to the logic.
Testing notes:
AYON_API_KEY-> start tray and launch application -> logout from web (to invalidate the token) -> do something in tray -> it should not create tons of requests in server.