`auth login`'s OAuth callback server can hang past its own timeout on a browser preconnect
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() < deadlinecheck is never reached again, so the 600-secondAUTH_TIMEOUT_SECONDSbound does not apply; - the real callback lands in the listen backlog and is never accepted;
browser-harness auth loginhangs withWaiting 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 acceptThreadingHTTPServer 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