mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(wrap): report unbindable proxy ports (#602)
This commit is contained in:
parent
18925b8c6e
commit
6dfcaa839f
2 changed files with 54 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue