fix(wrap): report unbindable proxy ports (#602)

This commit is contained in:
oxura 2026-06-05 07:19:00 +06:00 committed by GitHub
parent 18925b8c6e
commit 6dfcaa839f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 0 deletions

View file

@ -141,6 +141,29 @@ def _check_proxy(port: int) -> bool:
return False
def _port_bind_error(port: int) -> OSError | None:
"""Return the bind error for a local proxy port, or None when it is usable."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", port))
except OSError as exc:
return exc
return None
def _format_unbindable_port_error(port: int, error: OSError, agent_type: str) -> str:
"""Build an actionable message for ports that fail before uvicorn can bind."""
command = "headroom proxy"
if agent_type != "unknown":
command = f"headroom wrap {agent_type}"
suggested_port = port + 1
return (
f"Port {port} is unavailable on 127.0.0.1 before the proxy can start: {error}. "
"On Windows this can happen when the port is in an excluded or reserved range. "
f"Rerun with a different port, for example `{command} --port {suggested_port}`."
)
def _get_log_path() -> Path:
"""Get path for proxy log file."""
from headroom import paths as _paths
@ -1740,6 +1763,12 @@ def _ensure_proxy(
return None
# Start (or restart) the proxy with the requested flags
bind_error = helpers._port_bind_error(port)
if bind_error is not None:
raise click.ClickException(
helpers._format_unbindable_port_error(port, bind_error, agent_type)
)
click.echo(f" Starting Headroom proxy on port {port}...")
try:
proc = cast(

View file

@ -88,6 +88,31 @@ def test_ensure_proxy_falls_back_when_persistent_manifest_is_stale(monkeypatch)
assert calls == ["start"]
def test_ensure_proxy_reports_unbindable_port_before_starting_subprocess(monkeypatch) -> None:
calls: list[str] = []
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_port_bind_error",
lambda port: PermissionError(10013, "access denied by OS port reservation"),
)
monkeypatch.setattr(wrap_cli, "_start_proxy", lambda *args, **kwargs: calls.append("start"))
try:
wrap_cli._ensure_proxy(8787, False, agent_type="cursor")
except click.ClickException as exc:
message = str(exc)
else:
raise AssertionError("expected unbindable port to raise before starting proxy")
assert "Port 8787 is unavailable" in message
assert "Windows" in message
assert "headroom wrap cursor --port 8788" in message
assert calls == []
def test_ensure_proxy_restarts_idle_stale_persistent_deployment(monkeypatch) -> None:
calls: list[str] = []
health = {