{"record":{"id":"f8822c532f7cc208","repo":"libnyanpasu/clash-nyanpasu","slug":"clash-config-actor-reply-dropped","errorCode":null,"errorMessage":"clash config actor reply dropped","messagePattern":"clash config actor reply dropped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/tauri/src/client/clash_config.rs","lineNumber":126,"sourceCode":"        self.replace_prepared_if_version(expected_version, prepared)\n            .await\n    }\n\n    pub(crate) async fn prepare_replace(\n        &self,\n        state: ClashConfig,\n    ) -> anyhow::Result<PreparedTypedReplace<ClashConfig>> {\n        match self\n            .inner\n            .actor_ref\n            .call(\n                |reply| ClashConfigActorMessage::PrepareReplace { state, reply },\n                None,\n            )\n            .await?\n        {\n            CallResult::Success(result) => result,\n            CallResult::SenderError => anyhow::bail!(\"clash config actor reply dropped\"),\n            CallResult::Timeout => anyhow::bail!(\"clash config actor call timed out\"),\n        }\n    }\n\n    pub(crate) async fn replace_prepared_if_version(\n        &self,\n        expected_version: u64,\n        prepared: PreparedTypedReplace<ClashConfig>,\n    ) -> anyhow::Result<ConditionalReplaceResult<ClashConfigSnapshot>> {\n        match self\n            .inner\n            .actor_ref\n            .call(\n                |reply| ClashConfigActorMessage::ReplacePreparedIfVersion {\n                    expected_version,\n                    prepared,\n                    reply,\n                },","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/client/clash_config.rs#L108-L144","documentation":"This error is raised by `ClashConfigClient::prepare_replace` when the underlying ractor `actor_ref.call(...)` returns `CallResult::SenderError`. That means the RPC reply port was dropped before the clash-config actor sent a response — typically because the actor was stopped, crashed, or its message handler exited without calling the reply port. The client cannot return a `PreparedTypedReplace<ClashConfig>`, so it bails with this message.","triggerScenarios":"Calling `replace_if_version` (which internally calls `prepare_replace`) while the ClashConfigActor is stopped or dead — e.g. after `ClashConfigClientInner::drop` invoked `actor_ref.stop(None)`, after the actor panicked handling an earlier message, or if the actor's PrepareReplace handler returned without replying (e.g. legacy bridge `prepare()` failed and the reply port was dropped along the error path).","commonSituations":"App shutdown racing a config replace; a panic in the actor's persistent-state manager or legacy bridge killing the actor; sending a replace through a client clone whose actor was already torn down; an actor handler bug that forgets `reply.send(...)` on an error branch.","solutions":["Check that the ClashConfigActor is still alive before the call (or that the ClashConfigClient was not dropped, since Drop stops the actor).","Inspect the actor's handler for ClashConfigActorMessage::PrepareReplace and ensure every code path — including error/early-return paths — sends on the RpcReplyPort.","Add panic handling / supervision around the actor so a panic in the legacy bridge or persistence layer doesn't silently kill the actor mid-RPC.","Retry the operation by rebuilding the client via `ClashConfigClient::new` if the actor was stopped during shutdown."],"exampleFix":"// before: handler drops reply on error\nClashConfigActorMessage::PrepareReplace { state, reply } => {\n    let prepared = state.with_bridge(&bridge)?; // early `?` drops `reply`\n    reply.send(prepared)?;\n}\n// after: always reply\nClashConfigActorMessage::PrepareReplace { state, reply } => {\n    let _ = reply.send(state.with_bridge(&bridge).map_err(|e| anyhow::anyhow!(e.to_string())));\n}","handlingStrategy":"try-catch","validationCode":"// Rust: no pre-call validation is possible; guard the call site instead\nif client_is_shutting_down() {\n    anyhow::bail!(\"skipping prepare_replace: actor is being torn down\");\n}","typeGuard":null,"tryCatchPattern":"match client.replace_if_version(expected_version, next).await {\n    Ok(ConditionalReplaceResult::Replaced(s)) => { /* committed */ }\n    Ok(ConditionalReplaceResult::Conflict { actual_version }) => { /* re-read and retry */ }\n    Err(e) if e.to_string().contains(\"reply dropped\") => {\n        // actor stopped or panicked: rebuild the client, then retry once\n        let client = rebuild_client().await?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never drop the ClashConfigClient while a replace_if_version call is in flight.","Ensure every actor handler branch sends on the RpcReplyPort, including error paths.","Add supervision/panic hooks around the actor so panics surface in logs, not silent SenderError.","Keep the legacy bridge `prepare()` cheap and infallible, or reply with an Err instead of dropping the port."],"tags":["actor","rpc","rust","ractor","dropped-reply-port"],"backgroundTag":"actor-reply-dropped","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}