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.
Labels: bug, auth
Description
The browser login flow serves its OAuth redirect from a single-threaded stdlib
HTTPServeron the main thread:HTTPServer.timeout = 0.5bounds only theselect()beforeaccept(). Once aconnection is accepted,
handle_request()calls into the handler, andBaseHTTPRequestHandler.timeoutis never set — so the socket has no timeout andself.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>/...ispredicted, and typically opens more sockets than it uses. If
accept()returns one ofthose before the real request arrives,
handle_request()parks on a socket that willnever speak, and:
time.time() < deadlinecheck is never reached again, so the 600-secondAUTH_TIMEOUT_SECONDSbound does not apply;browser-harness auth loginhangs withWaiting for login to complete...and noerror, 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:
ThreadingHTTPServeris inhttp.serveralongside the import already there, andPendingCallbackis only written from the handler and read from the loop, so no extralocking is needed for the existing fields.
Secondary, same file
do_GETreturnsBrowser Use Cloud login completefor every request that reachesCALLBACK_PATH, includinginvalid_stateand provider-error callbacks(
auth.py:400-417). The user is told in the browser that login worked while the CLIprints
auth failed: invalid_state. The body should reflectcallback.error.