google-auth-library-python icon indicating copy to clipboard operation
google-auth-library-python copied to clipboard

async AuthorizedSession refresh should support async credential refresh method

Open thehesiod opened this issue 4 years ago • 3 comments

Recently I migrated from my custom async implementation to the beta implementation however I noticed two issues:

  1. _CombinedResponse is missing the reason property and occasionally errors out, my fix:
class _CombinedResponse(_GoogleCombinedResponse):
    @property
    def reason(self):
        return self._response.reason
  1. the credentials refresh does not support an async call (which should be the default if you're using the credentials_async class

fix is making this call async: https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/transport/_aiohttp_requests.py#L371

if sync needs to be supported it should do a check if the method is awaitable/a coroutine or not.

Environment details

  • OS: OSX
  • Python version: 3.8
  • pip version:
  • google-auth version: 2.2.1

Steps to reproduce

  1. auth error during googleapiclient call

thehesiod avatar Oct 26 '21 20:10 thehesiod

This is still an issue, and @thehesiod is right about the fix.

Some more detail...

It looks like the _aio_http_requests.AuthorizedSession definitely expects an async credentials object when it calls before_request:

                await self.credentials.before_request(
                    auth_request, method, url, request_headers
                )

And then it later in the same function, it definitely seems to assume that the credentials are non-async - running the refresh in a background thread but not actually awaiting the coroutine itself:

                    async with self._refresh_lock:
                        await self._loop.run_in_executor(
                            None, self.credentials.refresh, auth_request
                        )

Note: The await here isn't awaiting the refresh call; it's awaiting the background thread without awaiting the refresh coroutine itself.

So there seems to clearly be a paradox/contradiction here, conflating async vs non-async credentials.

Keep in mind that this only happens if the first HTTP request attempt fails with a retry status code, which is unlikely because the before_request also proactively does a refresh; but it is technically possible for the case to happen in the small window of time before the credentials expire, and quite nasty to diagnose when it does happen intermittently.

Fix:

                     async with self._refresh_lock:
-                        await self._loop.run_in_executor(
-                            None, self.credentials.refresh, auth_request
-                        )
+                        await self.credentials.refresh(auth_request)

Future(?)

A more fancy approach would be to conditionally await the refresh OR run a background thread, depending on whether async vs non-async credentials are used; but that's a bit more involved and might be more of a "nice to have" / future improvement.

Right now though the refresh call appears to definitely be broken, causing intermittently bizarre behavior when this code tries to trigger a refresh, so hopefully the quick fix can be applied.

(I don't have time to submit a PR for this, but maybe this info will help someone)

kkroening avatar May 05 '23 06:05 kkroening

Verified this is still a problem with v. 2.20.0.

eevelweezel avatar Jun 21 '23 16:06 eevelweezel