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

NotConnected

NotConnected

Error message

connection closed

What it means

The relay transport's receive channel was closed and drained (poll_recv_queue returned None), meaning the relay connection is gone. poll_recv surfaces this as io::ErrorKind::NotConnected with 'connection closed' and logs an error.

Solutions

  1. Treat NotConnected on the relay transport as terminal: stop polling and tear down/rebuild the endpoint or transport.
  2. Implement automatic reconnection to the relay when the connection drops, before resuming recv.
  3. Coordinate shutdown so no task polls recv after the relay connection is closed.
  4. Check relay server logs/health if drops are frequent (timeouts, restarts).

Example fix

// before
loop { let pkt = transport.poll_recv(cx)?; } // NotConnected after relay drop
// after
match transport.poll_recv(cx) {
    Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::NotConnected => { reconnect_relay().await?; }
    other => return other,
}
Defensive patterns

Strategy: fallback

Type guard

fn relay_alive(t: &RelayTransport) -> bool { !t.recv_channel_closed() }

Try / catch

match transport.poll_recv(cx) {
    Poll::Ready(Err(e)) if e.kind() == std::io::ErrorKind::NotConnected => { schedule_relay_reconnect(); Poll::Pending }
    other => other,
}

Prevention

When it happens

Trigger: Calling poll_recv on the relay transport after the relay connection terminated — the relay recv channel sent its final item and closed, so any subsequent recv hits Poll::Ready(None).

Common situations: Relay server disconnecting the client (idle timeout, server restart), network loss ending the relay WebSocket, application shutting down the endpoint while another task still polls recv, forgetting to reconnect after a relay drop.

Understand the failure class

Related errors


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

Appendix: source

Thrown at iroh/src/socket/transports/relay.rs:113

        metas: &mut [noq_udp::RecvMeta],
        recv_infos: &mut [RecvInfo],
    ) -> Poll<io::Result<usize>> {
        assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas");
        assert_eq!(
            bufs.len(),
            recv_infos.len(),
            "non matching bufs & recv_infos"
        );
        let mut num_msgs = 0;
        for i in 0..bufs.len() {
            let buf_out = &mut bufs[i];
            let meta_out = &mut metas[i];
            let recv_info = &mut recv_infos[i];
            let dm = match self.poll_recv_queue(cx) {
                Poll::Ready(Some(recv)) => recv,
                Poll::Ready(None) => {
                    error!("relay_recv_channel closed");
                    return Poll::Ready(Err(io::Error::new(
                        io::ErrorKind::NotConnected,
                        "connection closed",
                    )));
                }
                Poll::Pending => {
                    break;
                }
            };

            // This *tries* to make the datagrams fit into our buffer by re-batching them.
            let num_segments = dm
                .datagrams
                .segment_size
                .map_or(1, |ss| buf_out.len() / u16::from(ss) as usize);
            let datagrams = dm.datagrams.take_segments(num_segments);
            let empty_now = datagrams.contents.is_empty();
            let empty_after = dm.datagrams.contents.is_empty();

View on GitHub (pinned to 2b4de030ce)