feat(auth): send a request again when its token lapsed in the queue

A request can wait to be served for longer than the access token it was
sent with lives, and comes back refused for a token that was valid when
it left. Uploads that take minutes make that wait ordinary.

- tell an expired token apart from any other refusal by the code the
  API returns, and retry only that one, once
- prefer a token another process left behind over minting a second: it
  saves a round trip, and a rotating provider would retire one that is
  still in use. Assign it rather than read it, since the setter is what
  rewrites the session header the retry will carry
- settle the token before the request so the one compared afterwards is
  the one that was actually sent
- report an overloaded server when the second attempt is refused too,
  and let API errors out of the upload path with their own type
This commit is contained in:
2026-08-14 18:37:41 +09:00
parent 0cac30ccf8
commit e5c28835b8
9 changed files with 226 additions and 10 deletions
+14 -2
View File
@@ -8,8 +8,8 @@ from pydantic.dataclasses import dataclass
from requests_toolbelt.multipart.encoder import MultipartEncoder
from mdrsclient.api.base import BaseApi
from mdrsclient.api.utils import token_check
from mdrsclient.exceptions import UnexpectedException
from mdrsclient.api.utils import retry_on_expired_token, token_check
from mdrsclient.exceptions import MDRSException, UnexpectedException
from mdrsclient.models import File
@@ -30,6 +30,7 @@ class FilesApi(BaseApi):
ENTRYPOINT: Final[str] = "v3/files/"
FALLBACK_MIMETYPE: Final[str] = "application/octet-stream"
@retry_on_expired_token
def list(self, folder_id: str, page_num: int) -> FilesApiListResponse:
url = self.ENTRYPOINT
token_check(self.connection)
@@ -38,6 +39,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return TypeAdapter(FilesApiListResponse).validate_python(response.json())
@retry_on_expired_token
def retrieve(self, id: str) -> File:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + id + "/"
@@ -46,6 +48,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return TypeAdapter(File).validate_python(response.json())
@retry_on_expired_token
def create(self, folder_id: str, path: str) -> str:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT
@@ -66,10 +69,14 @@ class FilesApi(BaseApi):
raise UnexpectedException(f"Could not open `{path}` file.")
except MemoryError:
raise UnexpectedException("Out of memory.")
except MDRSException:
# Already says what went wrong, and the caller may want to act on the kind.
raise
except Exception as e:
raise UnexpectedException("Unspecified error.") from e
return ret.id
@retry_on_expired_token
def update(self, file: File, path: str | None) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/"
@@ -96,6 +103,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return True
@retry_on_expired_token
def destroy(self, file: File) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/"
@@ -104,6 +112,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return True
@retry_on_expired_token
def move(self, file: File, folder_id: str, name: str) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/move/"
@@ -113,6 +122,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return True
@retry_on_expired_token
def copy(self, file: File, folder_id: str, name: str) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/copy/"
@@ -122,6 +132,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return True
@retry_on_expired_token
def metadata(self, file: File) -> dict[str, Any]:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = self.ENTRYPOINT + file.id + "/metadata/"
@@ -130,6 +141,7 @@ class FilesApi(BaseApi):
self._raise_response_error(response)
return response.json()
@retry_on_expired_token
def download(self, file: File, path: str) -> bool:
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
url = file.download_url