fix(cli): display user-specified address in serve output (#5481)

* fix(cli): display user-specified address in serve output

When running `dx serve --addr 0.0.0.0`, the CLI was overriding the
displayed address to 127.0.0.1. This was confusing for users who
explicitly chose 0.0.0.0 and expected the output to reflect their
choice. The default (127.0.0.1) is unchanged when no --addr flag is
given.

Fixes #5477

* fix(cli): resolve actual network IP when bound to 0.0.0.0

Instead of showing the unroutable 0.0.0.0, use the UDP socket trick
to resolve the machine's primary network address for a clickable URL.
Falls back to 127.0.0.1 when resolution fails (no network, etc).

* style: apply rustfmt to test function calls

* fit longer urls in tui frame

* space

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
John CostaandJonathan Kelley authored and GitHub committed 2026-04-14 16:52:43 -07:00
1 parent 79693f8cff
commit cb5abb48dd
2 files changed
+27 -25

No files matched your search

+4 -17
View File
@@ -31,7 +31,7 @@ use tracing::Level;
use super::AppServer; use super::AppServer;
const TICK_RATE_MS: u64 = 100; const TICK_RATE_MS: u64 = 100;
const VIEWPORT_MAX_WIDTH: u16 = 90; const VIEWPORT_MAX_WIDTH: u16 = 92;
const VIEWPORT_HEIGHT_SMALL: u16 = 5; const VIEWPORT_HEIGHT_SMALL: u16 = 5;
const VIEWPORT_HEIGHT_BIG: u16 = 14; const VIEWPORT_HEIGHT_BIG: u16 = 14;
@@ -740,14 +740,7 @@ impl Output {
}; };
frame.render_widget_ref( frame.render_widget_ref(
Paragraph::new(Line::from(vec![ Paragraph::new(Line::from(vec!["Address: ".gray(), address])),
if client.build.bundle == BundleFormat::Web {
"Serving at: ".gray()
} else {
"Server at: ".gray()
},
address,
])),
serve_address, serve_address,
); );
} }
@@ -823,14 +816,8 @@ impl Output {
meta_list[3], meta_list[3],
); );
let server_address = match state.server.server_address() { // space
Some(address) => format!("http://{address}").yellow(), frame.render_widget(Paragraph::new(Line::from(vec![" ".gray()])), meta_list[4]);
None => "no address".dark_gray(),
};
frame.render_widget(
Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])),
meta_list[4],
);
let links_list: [_; 2] = let links_list: [_; 2] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom); Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom);
+23 -8
View File
@@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize};
use std::{ use std::{
convert::Infallible, convert::Infallible,
fs, io, fs, io,
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket},
path::Path, path::Path,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::Duration, time::Duration,
@@ -54,7 +54,6 @@ use super::AppServer;
/// and better tooling on the pages that we serve. /// and better tooling on the pages that we serve.
pub(crate) struct WebServer { pub(crate) struct WebServer {
devserver_exposed_ip: IpAddr, devserver_exposed_ip: IpAddr,
devserver_bind_ip: IpAddr,
devserver_port: u16, devserver_port: u16,
proxied_port: Option<u16>, proxied_port: Option<u16>,
hot_reload_sockets: Vec<ConnectedWsClient>, hot_reload_sockets: Vec<ConnectedWsClient>,
@@ -121,7 +120,6 @@ impl WebServer {
Ok(Self { Ok(Self {
build_status, build_status,
proxied_port, proxied_port,
devserver_bind_ip,
devserver_exposed_ip, devserver_exposed_ip,
devserver_port, devserver_port,
hot_reload_sockets: Default::default(), hot_reload_sockets: Default::default(),
@@ -389,23 +387,40 @@ impl WebServer {
} }
} }
/// Get the address the server is running - showing 127.0.0.1 if the devserver is bound to 0.0.0.0 /// Get the address to display to the user as a clickable link.
/// This is designed this way to not confuse users who expect the devserver to be bound to localhost ///
/// ... which it is, but they don't know that 0.0.0.0 also serves localhost. /// When bound to 0.0.0.0, resolves the machine's primary network address so the user
/// sees a real clickable URL instead of the unroutable 0.0.0.0.
pub fn displayed_address(&self) -> Option<SocketAddr> { pub fn displayed_address(&self) -> Option<SocketAddr> {
let mut address = self.server_address()?; let mut address = self.server_address()?;
// Set the port to the devserver port since that's usually what people expect // Set the port to the devserver port since that's usually what people expect
address.set_port(self.devserver_port); address.set_port(self.devserver_port);
if self.devserver_bind_ip == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { if address.ip().is_unspecified() {
address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), address.port()); match resolve_local_ip() {
Some(ip) => address.set_ip(ip),
None => {
tracing::debug!("Could not resolve local network IP; displaying localhost");
address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
}
}
} }
Some(address) Some(address)
} }
} }
/// Resolve the machine's primary local IP by asking the OS which interface routes to the internet.
///
/// Connects a UDP socket to a public address (no traffic is sent) and reads back the local address
/// the OS selected. Returns `None` if the lookup fails (e.g. no network connectivity).
fn resolve_local_ip() -> Option<IpAddr> {
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
socket.connect("8.8.8.8:80").ok()?;
Some(socket.local_addr().ok()?.ip())
}
async fn devserver_mainloop( async fn devserver_mainloop(
https_cfg: WebHttpsConfig, https_cfg: WebHttpsConfig,
listener: TcpListener, listener: TcpListener,