Skip to content

auth login's OAuth callback server can hang past its own timeout on a browser preconnect #691

Description

@rajarshidattapy

Labels: bug, auth

Description

The browser login flow serves its OAuth redirect from a single-threaded stdlib
HTTPServer on the main thread:

# src/browser_harness/auth.py:421
return HTTPServer(("127.0.0.1", 0), Handler)
# src/browser_harness/auth.py:249-256
def complete_browser_auth(start, *, timeout=AUTH_TIMEOUT_SECONDS):
    deadline = time.time() + timeout
    start.server.timeout = 0.5
    try:
        while not start.callback.complete and time.time() < deadline:
            start.server.handle_request()

HTTPServer.timeout = 0.5 bounds only the select() before accept(). Once a
connection is accepted, handle_request() calls into the handler, and
BaseHTTPRequestHandler.timeout is never set — so the socket has no timeout and
self.rfile.readline() blocks until the peer sends a request line or closes.

Impact

Browsers routinely open speculative TCP connections that carry no request. Chrome
preconnects to the origin as soon as a navigation to http://127.0.0.1:<port>/... is
predicted, and typically opens more sockets than it uses. If accept() returns one of
those before the real request arrives, handle_request() parks on a socket that will
never speak, and:

  • the time.time() < deadline check is never reached again, so the 600-second
    AUTH_TIMEOUT_SECONDS bound does not apply;
  • the real callback lands in the listen backlog and is never accepted;
  • browser-harness auth login hangs with Waiting for login to complete... and no
    error, until the browser eventually drops the idle socket (which can be minutes) or
    the user gives up.

The failure is intermittent and timing-dependent, which makes it look like a flaky
server-side auth problem rather than a client bug.

Suggested fix

Two one-line changes, either of which closes the hole; both together is better:

class Handler(BaseHTTPRequestHandler):
    timeout = 10          # bounds readline() on an accepted socket
    ...

return ThreadingHTTPServer(("127.0.0.1", 0), Handler)   # a silent socket can't block the next accept

ThreadingHTTPServer is in http.server alongside the import already there, and
PendingCallback is only written from the handler and read from the loop, so no extra
locking is needed for the existing fields.

Secondary, same file

do_GET returns Browser Use Cloud login complete for every request that reaches
CALLBACK_PATH, including invalid_state and provider-error callbacks
(auth.py:400-417). The user is told in the browser that login worked while the CLI
prints auth failed: invalid_state. The body should reflect callback.error.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions