← back to Exo
fix: use configured api_port for IP connectivity probes (#1877)
8973503322c2f377a4f84da135f92a71fd0dfe56 · 2026-04-14 00:01:42 +1000 · chaoliang yan
## Motivation
Fixes #1861
When `--api-port` is set to a non-default value (e.g., `--api-port
55555`), the IP connectivity discovery system still probes peers on the
hardcoded default port 52415. Since the API is not listening on 52415,
all reachability checks fail, the topology reports zero reachable nodes,
and the dashboard shows "No valid configurations for current settings."
## Changes
Thread the configured `api_port` from `Args` through `Worker` into the
reachability probe functions:
- `net_profile.py`: `check_reachability()` and `check_reachable()`
accept an `api_port` parameter (default 52415 for backward
compatibility)
- `worker/main.py`: `Worker` stores `api_port` and passes it to
`check_reachable()`, uses it in `Multiaddr` construction and the mDNS
connection filter
- `main.py`: passes `args.api_port` to the `Worker` constructor
## Why It Works
The `/node_id` endpoint used by reachability probes is served by the
FastAPI app, which binds to `args.api_port`. The probes must use the
same port the API is actually listening on. Before this fix, the port
was hardcoded in three places in `net_profile.py` and `worker/main.py`;
now it uses the value from the CLI flag.
## Test Plan
### Manual Testing
<!-- Hardware: not available for multi-node testing -->
- Verified ruff passes on all changed files
- Code inspection: traced `api_port` flow from `Args.parse()` →
`Node.create()` → `Worker.__init__()` → `_poll_connection_updates()` →
`check_reachable()` → `check_reachability()` → HTTP probe URL
### Automated Testing
- No existing automated tests cover the reachability probe code path
- The new `api_port` parameter defaults to `52415`, so all existing
behavior is preserved when `--api-port` is not specified
---------
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: Evan <evanev7@gmail.com>
Files touched
M src/exo/main.pyM src/exo/utils/info_gatherer/net_profile.pyM src/exo/worker/main.py
Diff
commit 8973503322c2f377a4f84da135f92a71fd0dfe56
Author: chaoliang yan <z5643222@ad.unsw.edu.au>
Date: Tue Apr 14 00:01:42 2026 +1000
fix: use configured api_port for IP connectivity probes (#1877)
## Motivation
Fixes #1861
When `--api-port` is set to a non-default value (e.g., `--api-port
55555`), the IP connectivity discovery system still probes peers on the
hardcoded default port 52415. Since the API is not listening on 52415,
all reachability checks fail, the topology reports zero reachable nodes,
and the dashboard shows "No valid configurations for current settings."
## Changes
Thread the configured `api_port` from `Args` through `Worker` into the
reachability probe functions:
- `net_profile.py`: `check_reachability()` and `check_reachable()`
accept an `api_port` parameter (default 52415 for backward
compatibility)
- `worker/main.py`: `Worker` stores `api_port` and passes it to
`check_reachable()`, uses it in `Multiaddr` construction and the mDNS
connection filter
- `main.py`: passes `args.api_port` to the `Worker` constructor
## Why It Works
The `/node_id` endpoint used by reachability probes is served by the
FastAPI app, which binds to `args.api_port`. The probes must use the
same port the API is actually listening on. Before this fix, the port
was hardcoded in three places in `net_profile.py` and `worker/main.py`;
now it uses the value from the CLI flag.
## Test Plan
### Manual Testing
<!-- Hardware: not available for multi-node testing -->
- Verified ruff passes on all changed files
- Code inspection: traced `api_port` flow from `Args.parse()` →
`Node.create()` → `Worker.__init__()` → `_poll_connection_updates()` →
`check_reachable()` → `check_reachability()` → HTTP probe URL
### Automated Testing
- No existing automated tests cover the reachability probe code path
- The new `api_port` parameter defaults to `52415`, so all existing
behavior is preserved when `--api-port` is not specified
---------
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: Evan <evanev7@gmail.com>
---
src/exo/main.py | 4 ++++
src/exo/utils/info_gatherer/net_profile.py | 8 +++++---
src/exo/worker/main.py | 9 ++++++---
3 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/exo/main.py b/src/exo/main.py
index 77dd4e58..49e21895 100644
--- a/src/exo/main.py
+++ b/src/exo/main.py
@@ -40,6 +40,7 @@ class Node:
node_id: NodeId
offline: bool
+ _api_port: int
_tg: TaskGroup = field(init=False, default_factory=TaskGroup)
@classmethod
@@ -98,6 +99,7 @@ class Node:
event_sender=event_router.sender(),
command_sender=router.sender(topics.COMMANDS),
download_command_sender=router.sender(topics.DOWNLOAD_COMMANDS),
+ api_port=args.api_port,
)
else:
worker = None
@@ -138,6 +140,7 @@ class Node:
api,
node_id,
args.offline,
+ args.api_port,
)
async def run(self):
@@ -250,6 +253,7 @@ class Node:
download_command_sender=self.router.sender(
topics.DOWNLOAD_COMMANDS
),
+ api_port=self._api_port,
)
self._tg.start_soon(self.worker.run)
if self.api:
diff --git a/src/exo/utils/info_gatherer/net_profile.py b/src/exo/utils/info_gatherer/net_profile.py
index 3255b207..d3d99164 100644
--- a/src/exo/utils/info_gatherer/net_profile.py
+++ b/src/exo/utils/info_gatherer/net_profile.py
@@ -19,13 +19,14 @@ async def check_reachability(
expected_node_id: NodeId,
out: dict[NodeId, set[str]],
client: httpx.AsyncClient,
+ api_port: int,
) -> None:
"""Check if a node is reachable at the given IP and verify its identity."""
if ":" in target_ip:
# TODO: use real IpAddress types
- url = f"http://[{target_ip}]:52415/node_id"
+ url = f"http://[{target_ip}]:{api_port}/node_id"
else:
- url = f"http://{target_ip}:52415/node_id"
+ url = f"http://{target_ip}:{api_port}/node_id"
remote_node_id = None
last_error = None
@@ -82,6 +83,7 @@ async def check_reachable(
topology: Topology,
self_node_id: NodeId,
node_network: Mapping[NodeId, NodeNetworkInfo],
+ api_port: int,
) -> AsyncGenerator[tuple[str, NodeId], None]:
"""Yield (ip, node_id) pairs as reachability probes complete."""
@@ -103,7 +105,7 @@ async def check_reachable(
) -> None:
async with send:
out: defaultdict[NodeId, set[str]] = defaultdict(set)
- await check_reachability(target_ip, expected_node_id, out, client)
+ await check_reachability(target_ip, expected_node_id, out, client, api_port)
if expected_node_id in out:
await send.send((target_ip, expected_node_id))
diff --git a/src/exo/worker/main.py b/src/exo/worker/main.py
index 573afb76..81c8c26c 100644
--- a/src/exo/worker/main.py
+++ b/src/exo/worker/main.py
@@ -71,12 +71,14 @@ class Worker:
# but I think it's the correct way to be thinking about commands
command_sender: Sender[ForwarderCommand],
download_command_sender: Sender[ForwarderDownloadCommand],
+ api_port: int,
):
self.node_id: NodeId = node_id
self.event_receiver = event_receiver
self.event_sender = event_sender
self.command_sender = command_sender
self.download_command_sender = download_command_sender
+ self.api_port = api_port
self.state: State = State()
self.runners: dict[RunnerId, RunnerSupervisor] = {}
@@ -393,16 +395,17 @@ class Worker:
self.state.topology,
self.node_id,
self.state.node_network,
+ api_port=self.api_port,
):
if ip in conns[nid]:
continue
conns[nid].add(ip)
edge = SocketConnection(
# nonsense multiaddr
- sink_multiaddr=Multiaddr(address=f"/ip4/{ip}/tcp/52415")
+ sink_multiaddr=Multiaddr(address=f"/ip4/{ip}/tcp/{self.api_port}")
if "." in ip
# nonsense multiaddr
- else Multiaddr(address=f"/ip6/{ip}/tcp/52415"),
+ else Multiaddr(address=f"/ip6/{ip}/tcp/{self.api_port}"),
)
if edge not in edges:
logger.debug(f"ping discovered {edge=}")
@@ -416,7 +419,7 @@ class Worker:
if not isinstance(conn.edge, SocketConnection):
continue
# ignore mDNS discovered connections
- if conn.edge.sink_multiaddr.port != 52415:
+ if conn.edge.sink_multiaddr.port != self.api_port:
continue
if (
conn.sink not in conns
← eb922861 models: add MiniMax M2.7 cards (#1884)
·
back to Exo
·
dashboard: group Gemma under Google with proper logo (#1883) d2f67b5d →