@@ -478,7 +478,7 @@ async def _handle_refresh_response(self, response: httpx.Response) -> bool:
478478 await self .context .storage .set_tokens (token_response )
479479
480480 return True
481- except ValidationError : # pragma: no cover
481+ except ValidationError :
482482 logger .exception ("Invalid refresh response" )
483483 self .context .clear_tokens ()
484484 return False
@@ -562,6 +562,11 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
562562 if self .context .is_token_valid ():
563563 self ._add_auth_header (request )
564564
565+ # Capture the access token actually used to send this request so the
566+ # 401 handler below can detect a token change made by a concurrent
567+ # request while this one was in flight.
568+ sent_access_token = self .context .current_tokens .access_token if self .context .current_tokens else None
569+
565570 response = yield request
566571
567572 # === Phase 4: 401 / 403 full OAuth flow ===
@@ -572,6 +577,17 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
572577 # here in the same pattern as Phase 1-2.
573578 if response .status_code == 401 :
574579 async with self .context .lock :
580+ # Concurrency guard: while this request was in flight, another
581+ # request holding ``context.lock`` may have already completed a
582+ # token refresh or a full re-authorization. If the stored access
583+ # token changed since we sent this request, the 401 is stale -
584+ # retry once with the new token instead of running a second,
585+ # duplicate ``authorization_code`` exchange.
586+ current_access_token = self .context .current_tokens .access_token if self .context .current_tokens else None
587+ if current_access_token is not None and current_access_token != sent_access_token :
588+ self ._add_auth_header (request )
589+ yield request
590+ return
575591 # Perform full OAuth flow
576592 try :
577593 # OAuth flow must be inline due to generator constraints
0 commit comments