{"record":{"id":"85b9bcecd9f0bf2b","repo":"n0-computer/iroh","slug":"connectionreset","errorCode":"ConnectionReset","errorMessage":"channel to actor is closed","messagePattern":"channel to actor is closed","errorType":"error_code","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"iroh/src/socket/transports/relay.rs","lineNumber":303,"sourceCode":"\n    pub(super) fn poll_send(\n        &mut self,\n        cx: &mut Context,\n        dest_url: RelayUrl,\n        dest_endpoint: EndpointId,\n        transmit: &Transmit<'_>,\n    ) -> Poll<io::Result<()>> {\n        match ready!(self.sender.poll_reserve(cx)) {\n            Ok(()) => {\n                let contents = datagrams_from_transmit(transmit);\n                let item = RelaySendItem {\n                    remote_endpoint: dest_endpoint,\n                    url: dest_url.clone(),\n                    datagrams: contents,\n                };\n                match self.sender.send_item(item) {\n                    Ok(()) => Poll::Ready(Ok(())),\n                    Err(_err) => Poll::Ready(Err(io::Error::new(\n                        io::ErrorKind::ConnectionReset,\n                        \"channel to actor is closed\",\n                    ))),\n                }\n            }\n            Err(_err) => Poll::Ready(Err(io::Error::new(\n                io::ErrorKind::ConnectionReset,\n                \"channel to actor is closed\",\n            ))),\n        }\n    }\n}\n\n/// Translate a UDP transmit to the `Datagrams` type for sending over the relay.\nfn datagrams_from_transmit(transmit: &Transmit<'_>) -> Datagrams {\n    Datagrams {\n        ecn: transmit.ecn.map(|ecn| match ecn {\n            noq_udp::EcnCodepoint::Ect0 => noq_proto::EcnCodepoint::Ect0,","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/n0-computer/iroh/blob/2b4de030ce5e0133f272871a76f0c685c63f552a/iroh/src/socket/transports/relay.rs#L285-L321","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nmatch self.sender.send_item(item) {\n    Ok(()) => Poll::Ready(Ok(())),\n    Err(_) => Poll::Ready(Err(io::Error::new(io::ErrorKind::ConnectionReset, \"channel to actor is closed\"))),\n}\n// after (caller side: recreate the connection on ConnectionReset)\nmatch sink.poll_send(cx, contents) {\n    Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::ConnectionReset => {\n        drop(sink); // channel to actor is closed; transport must be re-established\n        endpoint.force_reconnect(relay_url); // then retry on the new sink\n    }\n    other => other,\n}","handlingStrategy":"try-catch","validationCode":"// Before sending, check the sink/actor is still alive (channel not closed)\nfn relay_ready(sink: &RelaySink) -> bool { !sink.is_closed() } // if such an accessor exists; otherwise track actor JoinHandle::is_finished()","typeGuard":"// Track the actor task handle alongside the sink\nstruct RelayGuard { handle: tokio::task::JoinHandle<()>, sender: mpsc::Sender<RelayItem> }\nimpl RelayGuard {\n    fn is_alive(&self) -> bool { !self.handle.is_finished() }\n}","tryCatchPattern":"// Async code: inspect io::ErrorKind on send failure\nmatch sink.send(datagram).await {\n    Ok(()) => {},\n    Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {\n        // actor gone: recreate relay connection and retry once\n        endpoint.force_reconnect(relay_url);\n        let sink = endpoint.relay_sink();\n        sink.send(datagram).await?;\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["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)."],"tags":["network","relay","channel-closed","async"],"backgroundTag":"connection-refused","analyzedSha":"2b4de030ce5e0133f272871a76f0c685c63f552a","analyzedAt":"2026-09-08T04:26:47.755Z","contentChangedAt":"2026-09-08T04:26:47.755Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}