Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions google_auth_oauthlib/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,9 @@ def run_local_server(
in the user's browser.
redirect_uri_trailing_slash (bool): whether or not to add trailing
slash when constructing the redirect_uri. Default value is True.
timeout_seconds (int): It will raise an error after the timeout timing
if there are no credentials response. The value is in seconds.
timeout_seconds (int): It will raise a WSGITimeoutError exception after the
timeout timing if there are no credentials response. The value is in
seconds.
When set to None there is no timeout.
Default value is None.
token_audience (str): Passed along with the request for an access
Expand All @@ -425,6 +426,10 @@ def run_local_server(
Returns:
google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
for the user.

Raises:
WSGITimeoutError: If there is a timeout when waiting for the response from the
authorization server.
"""
wsgi_app = _RedirectWSGIApp(success_message)
# Fail fast if the address is occupied
Expand Down Expand Up @@ -455,7 +460,15 @@ def run_local_server(

# Note: using https here because oauthlib is very picky that
# OAuth 2.0 should only occur over https.
authorization_response = wsgi_app.last_request_uri.replace("http", "https")
try:
authorization_response = wsgi_app.last_request_uri.replace(
"http", "https"
)
except AttributeError as e:
raise WSGITimeoutError(
"Timed out waiting for response from authorization server"
) from e

self.fetch_token(
authorization_response=authorization_response, audience=token_audience
)
Expand Down Expand Up @@ -506,3 +519,7 @@ def __call__(self, environ, start_response):
start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
self.last_request_uri = wsgiref.util.request_uri(environ)
return [self._success_message.encode("utf-8")]


class WSGITimeoutError(AttributeError):
"""Raised when the WSGI server times out waiting for a response."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to Reviewer:

Why did we subclass on 'AttributeError'?

'AttributeError' was historically the error that was raised if this line failed:

authorization_response = wsgi_app.last_request_uri.replace("http", "https")

and thus we want to minimize the possibility for a breaking change if customers have code such as the following present in their apps.

try:
    data = server_obj.attribute
except AttributeError:
    # Handle the missing value gracefully
    data = DEFAULT_VALUE

Because of Python’s exception hierarchy, any code catching AttributeError will also catch WSGITimeoutError.

17 changes: 17 additions & 0 deletions tests/unit/test_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,20 @@ def test_run_local_server_logs_and_prints_url(
urllib.parse.quote(instance.redirect_uri, safe="")
in print_mock.call_args[0][0]
)

@mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
@mock.patch("wsgiref.simple_server.make_server", autospec=True)
def test_run_local_server_timeout(
self, make_server_mock, webbrowser_mock, instance, mock_fetch_token
):
mock_server = mock.Mock()
make_server_mock.return_value = mock_server

# handle_request does nothing (simulating timeout), so last_request_uri remains None
mock_server.handle_request.return_value = None

with pytest.raises(flow.WSGITimeoutError):
instance.run_local_server(timeout_seconds=1)

webbrowser_mock.get.assert_called_with(None)
webbrowser_mock.get.return_value.open.assert_called_once()
Loading