diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index df4de463a55..c32bb94b868 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -39,7 +39,7 @@ import webbrowser from contextlib import contextmanager from dataclasses import dataclass, field from datetime import datetime, timezone -from http.server import BaseHTTPRequestHandler, HTTPServer +from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple from urllib.parse import parse_qs, urlencode, urlparse @@ -2426,8 +2426,9 @@ def _xai_start_callback_server( expected_path = XAI_OAUTH_REDIRECT_PATH handler_cls, result = _make_xai_callback_handler(expected_path) - class _ReuseHTTPServer(HTTPServer): + class _ReuseHTTPServer(ThreadingHTTPServer): allow_reuse_address = True + daemon_threads = True ports_to_try = [preferred_port] if preferred_port != 0: diff --git a/tests/hermes_cli/test_auth_xai_oauth_provider.py b/tests/hermes_cli/test_auth_xai_oauth_provider.py index 9f1cc55f57e..76c1e6228fa 100644 --- a/tests/hermes_cli/test_auth_xai_oauth_provider.py +++ b/tests/hermes_cli/test_auth_xai_oauth_provider.py @@ -2,7 +2,9 @@ import base64 import json +import socket import time +import urllib.request from pathlib import Path import pytest @@ -20,6 +22,7 @@ from hermes_cli.auth import ( _xai_access_token_is_expiring, _xai_callback_cors_origin, _xai_oauth_build_authorize_url, + _xai_start_callback_server, _xai_validate_loopback_redirect_uri, get_xai_oauth_auth_status, refresh_xai_oauth_pure, @@ -278,6 +281,29 @@ def test_xai_callback_cors_origin_rejects_unknown_origin(): assert _xai_callback_cors_origin("") == "" +def test_xai_callback_server_accepts_fallback_code_while_browser_connection_is_stuck(): + """Regression: Chrome/xAI can leave a loopback connection open after + showing the Grok Build fallback code. A single-threaded callback server then + blocks forever and cannot accept the manual fallback callback. + """ + server, thread, result, redirect_uri = _xai_start_callback_server(preferred_port=0) + stuck = socket.create_connection((XAI_OAUTH_REDIRECT_HOST, server.server_address[1]), timeout=2) + try: + stuck.sendall(b"GET /callback?code=stuck") + callback_url = f"{redirect_uri}?code=fallback-code&state=state-123" + with urllib.request.urlopen(callback_url, timeout=2) as response: + body = response.read().decode("utf-8") + assert response.status == 200 + assert "xAI authorization received" in body + assert result["code"] == "fallback-code" + assert result["state"] == "state-123" + finally: + stuck.close() + server.shutdown() + server.server_close() + thread.join(timeout=1.0) + + # --------------------------------------------------------------------------- # Token roundtrip + reads # ---------------------------------------------------------------------------