clash-verge-rev/clash-verge-rev · error · anyhow::Error

listener address must include a port

Error message

listener address must include a port

What it means

Thrown by parse_listener_address when the input cannot be parsed as a single SocketAddr AND does not contain a `:` separator from which a host:port split can be derived. The parser requires either a full socket address or at minimum a `host:port` pair so it can bind listeners deterministically.

Source

Thrown at src-tauri/src/core/listener.rs:281

        .and_then(|address| address.strip_suffix(']'))
        .unwrap_or(bind_address);
    Ok(vec![
        IpAddr::from_str(normalized).with_context(|| format!("invalid bind-address {bind_address:?}"))?,
    ])
}

fn parse_listener_address(address: &str) -> Result<(Vec<IpAddr>, u16)> {
    let address = address.trim();
    if let Ok(socket) = SocketAddr::from_str(address) {
        if socket.port() == 0 {
            bail!("listener port must be between 1 and 65535");
        }
        return Ok((vec![socket.ip()], socket.port()));
    }

    let (host, port) = address
        .rsplit_once(':')
        .ok_or_else(|| anyhow!("listener address must include a port"))?;
    let port = port
        .parse::<u16>()
        .with_context(|| format!("invalid listener port {port:?}"))?;
    if port == 0 {
        bail!("listener port must be between 1 and 65535");
    }
    let host = host
        .trim()
        .strip_prefix('[')
        .and_then(|host| host.strip_suffix(']'))
        .unwrap_or_else(|| host.trim());
    if host.eq_ignore_ascii_case("localhost") {
        return Ok((vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], port));
    }
    Ok((
        vec![IpAddr::from_str(host).with_context(|| format!("invalid listener host {host:?}"))?],
        port,
    ))

View on GitHub (pinned to 5cad0f2799)

Solutions

  1. Provide the address in `host:port` form, e.g. `127.0.0.1:9090`.
  2. For IPv6, use bracketed form: `[::1]:9090`.
  3. Use the literal `localhost:PORT` if you want both v4/v6 loopback.
  4. Validate the address with `SocketAddr::from_str` before submitting it as a listener binding.

Example fix

// before
external-controller: 127.0.0.1
// after
external-controller: 127.0.0.1:9090
Defensive patterns

Strategy: validation

Validate before calling

fn parse_safe(address: &str) -> Result<(Vec<IpAddr>, u16), String> {
    use std::net::SocketAddr;
    let t = address.trim();
    if SocketAddr::from_str(t).is_ok() || t.rsplit_once(':').is_some() {
        crate::core::listener::parse_listener_address(t).map_err(|e| e.to_string())
    } else {
        Err("listener address must include a port, e.g. 127.0.0.1:9090".into())
    }
}

Type guard

fn looks_like_host_port(s: &str) -> bool {
    s.trim().rsplit_once(':').is_some()
}

Try / catch

if let Err(e) = parse_listener_address(&addr) {
    if e.to_string().contains("must include a port") {
        // prompt user for port before retrying
    }
}

Prevention

When it happens

Trigger: Passing a bare hostname or IP without a port (e.g. `127.0.0.1`, `localhost`); passing an empty string; passing an IPv6 address without brackets and port; passing a malformed mixed-port / external-controller / redir-port / tproxy-port / mixed-listener address.

Common situations: Clash/Mihomo YAML config sets `external-controller: 127.0.0.1` (missing `:9090`); user edits the mixed-port or bind address and drops the port; a profile import supplies a listener address field that omits the port.

Related errors


AI-assisted analysis of clash-verge-rev/clash-verge-rev@5cad0f2799 (2026-08-12). Data as JSON: /api/errors/ad0197fcdb62efb7. Report an issue: GitHub.