From cb5abb48dd667164f0aa65670181472710e22886 Mon Sep 17 00:00:00 2001 From: John Costa Date: Tue, 14 Apr 2026 16:52:43 -0700 Subject: [PATCH] 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 --- packages/cli/src/serve/output.rs | 21 ++++----------------- packages/cli/src/serve/server.rs | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 6a0637a11..76b15e364 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -31,7 +31,7 @@ use tracing::Level; use super::AppServer; 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_BIG: u16 = 14; @@ -740,14 +740,7 @@ impl Output { }; frame.render_widget_ref( - Paragraph::new(Line::from(vec![ - if client.build.bundle == BundleFormat::Web { - "Serving at: ".gray() - } else { - "Server at: ".gray() - }, - address, - ])), + Paragraph::new(Line::from(vec!["Address: ".gray(), address])), serve_address, ); } @@ -823,14 +816,8 @@ impl Output { meta_list[3], ); - let server_address = match state.server.server_address() { - Some(address) => format!("http://{address}").yellow(), - None => "no address".dark_gray(), - }; - frame.render_widget( - Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])), - meta_list[4], - ); + // space + frame.render_widget(Paragraph::new(Line::from(vec![" ".gray()])), meta_list[4]); let links_list: [_; 2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom); diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index 838ec9129..5ed32e06f 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use std::{ convert::Infallible, fs, io, - net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, + net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket}, path::Path, sync::{Arc, RwLock}, time::Duration, @@ -54,7 +54,6 @@ use super::AppServer; /// and better tooling on the pages that we serve. pub(crate) struct WebServer { devserver_exposed_ip: IpAddr, - devserver_bind_ip: IpAddr, devserver_port: u16, proxied_port: Option, hot_reload_sockets: Vec, @@ -121,7 +120,6 @@ impl WebServer { Ok(Self { build_status, proxied_port, - devserver_bind_ip, devserver_exposed_ip, devserver_port, 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 - /// 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. + /// Get the address to display to the user as a clickable link. + /// + /// 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 { let mut address = self.server_address()?; // Set the port to the devserver port since that's usually what people expect address.set_port(self.devserver_port); - if self.devserver_bind_ip == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { - address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), address.port()); + if address.ip().is_unspecified() { + 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) } } +/// 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 { + 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( https_cfg: WebHttpsConfig, listener: TcpListener,