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

ConnectionReset

ConnectionReset

Error message

channel to actor is closed

What it means

In iroh's relay transport, poll_send forwards datagrams to a dedicated actor task via an mpsc channel (self.sender.send_item). This io::Error with ConnectionReset is produced when the receiving end of that channel has been dropped — the relay actor has shut down — so the datagram cannot be queued. It signals the relay connection is no longer usable and the socket will typically be torn down or re-established.

Solutions

  1. Treat this as a dead transport: stop using this sink and let the endpoint reconnect (the magicsock/transport layer recreates the relay actor).
  2. Check that the relay actor task isn't being cancelled prematurely (e.g. an aborted JoinHandle or dropped supervisor) in your embedding code.
  3. Retry the send on a newly established relay connection instead of the stale sink.
  4. Inspect relay actor logs for the underlying error that made it exit (connection failure, timeout).

Example fix

// before
match self.sender.send_item(item) {
    Ok(()) => Poll::Ready(Ok(())),
    Err(_) => Poll::Ready(Err(io::Error::new(io::ErrorKind::ConnectionReset, "channel to actor is closed"))),
}
// after (caller side: recreate the connection on ConnectionReset)
match sink.poll_send(cx, contents) {
    Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::ConnectionReset => {
        drop(sink); // channel to actor is closed; transport must be re-established
        endpoint.force_reconnect(relay_url); // then retry on the new sink
    }
    other => other,
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before sending, check the sink/actor is still alive (channel not closed)
fn relay_ready(sink: &RelaySink) -> bool { !sink.is_closed() } // if such an accessor exists; otherwise track actor JoinHandle::is_finished()

Type guard

// Track the actor task handle alongside the sink
struct RelayGuard { handle: tokio::task::JoinHandle<()>, sender: mpsc::Sender<RelayItem> }
impl RelayGuard {
    fn is_alive(&self) -> bool { !self.handle.is_finished() }
}

Try / catch

// Async code: inspect io::ErrorKind on send failure
match sink.send(datagram).await {
    Ok(()) => {},
    Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {
        // actor gone: recreate relay connection and retry once
        endpoint.force_reconnect(relay_url);
        let sink = endpoint.relay_sink();
        sink.send(datagram).await?;
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling poll_send on the relay sink after the relay actor task has exited (actor loop ended, handle dropped, actor returned an error), so Sender::send_item returns Err.

Common situations: Relay server connection dropped or timed out; node shutdown racing with in-flight writes; supervisor cancelled the actor task; network change caused the transport to be replaced while a caller still holds the old sink.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


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

Appendix: source

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

    pub(super) fn poll_send(
        &mut self,
        cx: &mut Context,
        dest_url: RelayUrl,
        dest_endpoint: EndpointId,
        transmit: &Transmit<'_>,
    ) -> Poll<io::Result<()>> {
        match ready!(self.sender.poll_reserve(cx)) {
            Ok(()) => {
                let contents = datagrams_from_transmit(transmit);
                let item = RelaySendItem {
                    remote_endpoint: dest_endpoint,
                    url: dest_url.clone(),
                    datagrams: contents,
                };
                match self.sender.send_item(item) {
                    Ok(()) => Poll::Ready(Ok(())),
                    Err(_err) => Poll::Ready(Err(io::Error::new(
                        io::ErrorKind::ConnectionReset,
                        "channel to actor is closed",
                    ))),
                }
            }
            Err(_err) => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::ConnectionReset,
                "channel to actor is closed",
            ))),
        }
    }
}

/// Translate a UDP transmit to the `Datagrams` type for sending over the relay.
fn datagrams_from_transmit(transmit: &Transmit<'_>) -> Datagrams {
    Datagrams {
        ecn: transmit.ecn.map(|ecn| match ecn {
            noq_udp::EcnCodepoint::Ect0 => noq_proto::EcnCodepoint::Ect0,

View on GitHub (pinned to 2b4de030ce)