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
- Treat this as a dead transport: stop using this sink and let the endpoint reconnect (the magicsock/transport layer recreates the relay actor).
- Check that the relay actor task isn't being cancelled prematurely (e.g. an aborted JoinHandle or dropped supervisor) in your embedding code.
- Retry the send on a newly established relay connection instead of the stale sink.
- 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
- Keep the relay actor task owned for the lifetime of the sink; never abort or drop its JoinHandle while sends may be pending.
- Treat io::ErrorKind::ConnectionReset on relay sends as 'reconnect', not 'retry in place'.
- Subscribe to the endpoint's connection/disconnection events and refresh cached sinks on relay reconnects.
- Monitor relay actor logs for the root exit cause (timeout, unreachable relay URL).
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
- 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/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)