googleapis / google-auth-library-python

Google Auth Python Library
https://googleapis.dev/python/google-auth/latest/
Apache License 2.0
776 stars 306 forks source link

async AuthorizedSession refresh should support async credential refresh method #901

Open thehesiod opened 3 years ago

thehesiod commented 3 years ago

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

2) 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

Steps to reproduce

  1. auth error during googleapiclient call
kkroening commented 1 year ago

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)

eevelweezel commented 1 year ago

Verified this is still a problem with v. 2.20.0.

inyutin commented 1 week ago

Verified this is still a problem with v2.32.0