n0-computer/iroh · error · Error

NoObservedAddr

Error message

NoObservedAddr

What it means

Error::NoObservedAddr from iroh-relay's QUIC address discovery (QAD). get_addr_and_latency awaits the relay/QUIC endpoint to report an externally observed address, and the stream ended without ever yielding one, so the bail fires. It means address discovery failed — the client could not learn its public address.

Solutions

  1. Retry address discovery against a responsive relay/QAD server.
  2. Check that UDP/QUIC traffic to the relay is not blocked by firewall or NAT.
  3. Fall back to other discovery mechanisms (e.g. relay-based observed address, STUN-like discovery) or configured external addresses.
  4. Update iroh versions on both ends to ensure compatible QUIC address-discovery behavior.

Example fix

match endpoint.discover_observed_addr().await {
    Ok((addr, latency)) => /* use addr */,
    Err(e) if e.to_string().contains("NoObservedAddr") => {
        // fall back to relay-reported address or retry another relay
    }
}
Defensive patterns

Strategy: fallback

Validate before calling

// Pre-check relay reachability before QAD
if tokio::time::timeout(DISCOVERY_TIMEOUT, conn probe).await.is_err() {
    // skip this relay, try another
}

Try / catch

match get_addr_and_latency(...).await {
    Err(e) if e.to_string().contains("NoObservedAddr") => use_fallback_addr_discovery(),
    other => other,
}

Prevention

When it happens

Trigger: Calling the QUIC address-discovery flow (get_addr_and_latency) when the server never sends an observed address: e.g. an unresponsive or misbehaving QAD server, a delayed connect that never completes the address-disc session, or the connection closing before external_addresses yields.

Common situations: NAT/firewall setups that block QUIC address discovery; relay servers that accept the connection but never answer address discovery; tests like test_qad_client_closes_unresponsive_fast and test_qad_connect_delayed simulate exactly these server misbehaviors.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


AI-assisted analysis of n0-computer/iroh@2b4de030ce (2026-09-08). Data as JSON: /api/errors/7f03fef03118970c. Report an issue: GitHub.

Appendix: source

Thrown at iroh-relay/src/quic.rs:342

        let conn = connecting?.await?;
        let mut external_addresses = conn.observed_external_addr();
        // TODO(ramfox): I'd like to be able to cancel this so we can close cleanly
        // if there the task that runs this function gets aborted.
        // tokio::select! {
        //     _ = cancel.cancelled() => {
        //         conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
        //         bail_any!("QUIC address discovery canceled early");
        //     },
        //     res = external_addresses.wait_for(|addr| addr.is_some()) => {
        //         let addr = res?.expect("checked");
        //         let latency = conn.rtt() / 2;
        //         // gracefully close the connections
        //         conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
        //         Ok((addr, latency))
        //     }

        let Some(mut observed_addr) = external_addresses.next().await else {
            n0_error::bail!(Error::NoObservedAddr);
        };
        // if we've sent to an ipv4 address, but received an observed address
        // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses)
        observed_addr = SocketAddr::new(observed_addr.ip().to_canonical(), observed_addr.port());
        let latency = conn.rtt(PathId::ZERO).unwrap_or_default();
        // gracefully close the connections
        conn.close(QUIC_ADDR_DISC_CLOSE_CODE, QUIC_ADDR_DISC_CLOSE_REASON);
        Ok((observed_addr, latency))
    }

    /// Create a connection usable for qad
    pub async fn create_conn(
        &self,
        server_addr: SocketAddr,
        host: &str,
    ) -> Result<noq::Connection, Error> {
        let config = self.client_config.clone();
        let connecting = self.ep.connect_with(config, server_addr, host);

View on GitHub (pinned to 2b4de030ce)