n0-computer/iroh · error · io::Error

NotConnected

NotConnected

Error message

connection closed

What it means

The noq multipath socket wrapper was asked to map a transmit's destination address while its underlying UDP socket was already closed. mapped_addr returns io::ErrorKind::NotConnected with 'connection closed' because sends can no longer be performed on a closed socket.

Solutions

  1. Ensure the QUIC endpoint/connection is fully shut down before closing the underlying UDP socket.
  2. Stop issuing transmits once the transport reports closed; treat NotConnected as terminal.
  3. Check for tasks holding the socket handle after shutdown and drop them.
  4. Guard sends with a liveness/connected check on the transport handle.

Example fix

// before
sock.send(transmit)?; // may hit closed socket
// after
if sock.is_closed() { return; }
sock.send(transmit)?;
Defensive patterns

Strategy: type-guard

Type guard

fn sendable(sock: &MultipathSock, t: &Transmit) -> Result<(), std::io::Error> {
    if sock.is_closed() { Err(std::io::Error::new(std::io::ErrorKind::NotConnected, "connection closed")) } else { Ok(()) }
}

Try / catch

match sock.send(transmit) {
    Err(e) if e.kind() == std::io::ErrorKind::NotConnected => { /* socket shut down: stop sending */ }
    other => other?,
}

Prevention

When it happens

Trigger: Transmitting a packet through the transport after the socket has been closed (sock.is_closed() == true) — typically when the endpoint/transport was shut down but a queued send or poll still references it.

Common situations: Endpoint dropped/closed while in-flight packets are still being flushed, shutdown ordering issues where the QUIC connection outlives its socket, cancelled tasks racing with socket close.

Understand the failure class

Related errors


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

Appendix: source

Thrown at iroh/src/socket/transports.rs:1156

/// [`Socket`] which expands it back to one or more [`Addr`]s and sends it
/// using the underlying [`Transports`].
#[derive(Debug)]
#[pin_project::pin_project]
pub(crate) struct Sender {
    sock: Arc<Socket>,
    #[pin]
    sender: TransportsSender,
}

impl Sender {
    /// Extracts the right [`Addr`] from the [`noq_udp::Transmit`].
    ///
    /// Because Noq does only know about IP transports we map other transports to private
    /// IPv6 Unique Local Address ranges.  This extracts the transport addresses out of the
    /// transmit's destination.
    fn mapped_addr(&self, transmit: &noq_udp::Transmit) -> io::Result<MultipathMappedAddr> {
        if self.sock.is_closed() {
            return Err(io::Error::new(
                io::ErrorKind::NotConnected,
                "connection closed",
            ));
        }

        Ok(MultipathMappedAddr::from(transmit.destination))
    }
}

impl noq::UdpSender for Sender {
    fn poll_send(
        self: Pin<&mut Self>,
        noq_transmit: &noq_udp::Transmit,
        cx: &mut Context,
    ) -> Poll<io::Result<()>> {
        // On errors this methods prefers returning Ok(()) to Noq.  Returning an error
        // should only happen if the error is permanent and fatal and it will never be
        // possible to send anything again.  Doing so kills the Noq EndpointDriver.  Most

View on GitHub (pinned to 2b4de030ce)