libnyanpasu/clash-nyanpasu · warning

Clash stream actor reply dropped

Error message

Clash stream actor reply dropped

What it means

The ractor RPC returned CallResult::SenderError, meaning the reply channel's sender was gone when the actor tried to answer — the caller's oneshot receiver was dropped mid-request. In practice this indicates the request future was cancelled/dropped before the actor replied, or the actor framework failed to deliver the response. Distinct from timeout: the actor did (try to) reply, but nobody was listening.

Source

Thrown at backend/tauri/src/core/clash/ws.rs:696

            actor,
            connections,
            events,
        })))
    }
    async fn call<T: Send + 'static>(
        &self,
        message: impl FnOnce(RpcReplyPort<T>) -> Message,
    ) -> Result<T> {
        match self
            .0
            .actor
            .call(message, Some(Duration::from_secs(10)))
            .await
            .context("Clash stream actor unavailable")?
        {
            CallResult::Success(value) => Ok(value),
            CallResult::Timeout => anyhow::bail!("Clash stream actor timed out"),
            CallResult::SenderError => anyhow::bail!("Clash stream actor reply dropped"),
        }
    }
    pub async fn start(&self) -> Result<()> {
        self.call(Message::Start).await
    }
    #[allow(dead_code)]
    pub async fn stop(&self) -> Result<()> {
        self.call(Message::Stop).await
    }
    pub async fn snapshot(&self) -> Result<ClashWsSnapshot> {
        self.call(Message::Snapshot).await
    }
    pub async fn set_recording(
        &self,
        kind: ClashWsKind,
        enabled: bool,
    ) -> Result<ClashWsRecording> {
        self.call(|reply| Message::Recording(kind, enabled, reply))

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Ensure callers await the call to completion or handle cancellation gracefully; avoid wrapping in select! that drops the call future mid-flight on timeouts
  2. Log at debug level and surface as retryable; resend the message once for idempotent operations
  3. Check actor reply code uses the correct RpcReplyPort and that State is not dropping ports early

Example fix

// before
let result = tokio::select! {
    r = client.start() => r?,
    _ = sleep(TIMEOUT) => bail!("outer timeout"),
};
// after
let result = client.start().await?; // let the actor's own 10s RPC timeout govern cancellation
Defensive patterns

Strategy: try-catch

Try / catch

match client.start().await {
    Err(e) if e.to_string().contains("reply dropped") => log::debug!("request cancelled before reply; safe to resend"),
    other => other?,
}

Prevention

When it happens

Trigger: The future calling .actor.call(...) is aborted (task cancelled, request dropped, timeout wrapper cancelling the outer future) before the actor sends its reply; the reply port's receiver side was dropped.

Common situations: Tauri command handlers cancelled by the frontend (window closed / request aborted); tokio tasks aborted on shutdown; racing timeouts in an outer layer that drops the inner call future.

Related errors


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