[object Object]

← 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

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 →