libnyanpasu/clash-nyanpasu · error

clash config actor reply dropped

Error message

clash config actor reply dropped

What it means

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.

Source

Thrown at backend/tauri/src/client/clash_config.rs:126

        self.replace_prepared_if_version(expected_version, prepared)
            .await
    }

    pub(crate) async fn prepare_replace(
        &self,
        state: ClashConfig,
    ) -> anyhow::Result<PreparedTypedReplace<ClashConfig>> {
        match self
            .inner
            .actor_ref
            .call(
                |reply| ClashConfigActorMessage::PrepareReplace { state, reply },
                None,
            )
            .await?
        {
            CallResult::Success(result) => result,
            CallResult::SenderError => anyhow::bail!("clash config actor reply dropped"),
            CallResult::Timeout => anyhow::bail!("clash config actor call timed out"),
        }
    }

    pub(crate) async fn replace_prepared_if_version(
        &self,
        expected_version: u64,
        prepared: PreparedTypedReplace<ClashConfig>,
    ) -> anyhow::Result<ConditionalReplaceResult<ClashConfigSnapshot>> {
        match self
            .inner
            .actor_ref
            .call(
                |reply| ClashConfigActorMessage::ReplacePreparedIfVersion {
                    expected_version,
                    prepared,
                    reply,
                },

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Check that the ClashConfigActor is still alive before the call (or that the ClashConfigClient was not dropped, since Drop stops the actor).
  2. Inspect the actor's handler for ClashConfigActorMessage::PrepareReplace and ensure every code path — including error/early-return paths — sends on the RpcReplyPort.
  3. 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.
  4. Retry the operation by rebuilding the client via `ClashConfigClient::new` if the actor was stopped during shutdown.

Example fix

// before: handler drops reply on error
ClashConfigActorMessage::PrepareReplace { state, reply } => {
    let prepared = state.with_bridge(&bridge)?; // early `?` drops `reply`
    reply.send(prepared)?;
}
// after: always reply
ClashConfigActorMessage::PrepareReplace { state, reply } => {
    let _ = reply.send(state.with_bridge(&bridge).map_err(|e| anyhow::anyhow!(e.to_string())));
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: no pre-call validation is possible; guard the call site instead
if client_is_shutting_down() {
    anyhow::bail!("skipping prepare_replace: actor is being torn down");
}

Try / catch

match client.replace_if_version(expected_version, next).await {
    Ok(ConditionalReplaceResult::Replaced(s)) => { /* committed */ }
    Ok(ConditionalReplaceResult::Conflict { actual_version }) => { /* re-read and retry */ }
    Err(e) if e.to_string().contains("reply dropped") => {
        // actor stopped or panicked: rebuild the client, then retry once
        let client = rebuild_client().await?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/f8822c532f7cc208. Report an issue: GitHub.