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

Author: rajarshidattapyCreated Aug 29, 2026Updated Aug 29, 2026

Labels: bug, auth

Description

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

python
# src/browser_harness/auth.py:421
return HTTPServer(("127.0.0.1", 0), Handler)
python
# 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:

python
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.

Source: browser-use/browser-harness