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
- Treat NotConnected on the relay transport as terminal: stop polling and tear down/rebuild the endpoint or transport.
- Implement automatic reconnection to the relay when the connection drops, before resuming recv.
- Coordinate shutdown so no task polls recv after the relay connection is closed.
- 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
- Reconnect to the relay automatically when the connection drops.
- Stop recv loops once NotConnected is observed; don't spin.
- Coordinate endpoint shutdown with all recv tasks.
- Watch for relay disconnects caused by idle timeouts and send periodic keep-alives.
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
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- The relay is rate-limiting this endpoint; outbound relay…
- Invalid transport configuration
- UnexpectedUpgradeStatus
- ExceedsMaxPacketSize
- EmptyPacket
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)