libnyanpasu/clash-nyanpasu · error

Clash stream actor timed out

Error message

Clash stream actor timed out

What it means

The Clash websocket/IPC bridge sends a typed message to a ractor actor with a 10-second RPC timeout and maps CallResult::Timeout to this error. It means the Clash stream actor received the request but did not reply within 10 seconds — the actor is busy, blocked, or its handler hung. Distinguish it from 'actor unavailable' (send failure) and 'reply dropped' (sender error).

Source

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

        Ok(Self(Arc::new(Inner {
            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> {

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Check the Clash core process is alive and the websocket endpoint is responsive; restart the core if hung
  2. Increase the 10s timeout only if the operation is legitimately slow — prefer diagnosing the stall first
  3. Inspect the actor's message loop for blocking calls or cross-actor cycles that can deadlock (avoid StateActor -> CoreActor -> StateActor patterns)
  4. Add retry-with-backoff at the caller for idempotent messages like Start

Example fix

// before
.actor.call(message, Some(Duration::from_secs(10)))
// after
match actor.call(message, Some(Duration::from_secs(10))).await {
    Ok(CallResult::Timeout) => log::warn!("clash stream actor timed out; retrying once"),
    other => return other.context("Clash stream actor unavailable"),
}
Defensive patterns

Strategy: retry

Validate before calling

// before calling: check core is reachable
// e.g. ping the clash controller endpoint with a short HTTP timeout

Try / catch

match client.start().await {
    Err(e) if e.to_string().contains("timed out") => retry_with_backoff(3, || client.start()),
    other => other,
}

Prevention

When it happens

Trigger: Calling start() (or other Message variants) on the clash ws client while the actor is stuck processing a prior message, the backend connection is stalled, or the actor task is starved on a blocked await for more than 10 seconds.

Common situations: Clash core hanging or slow to accept a websocket connection; network stalls to the core endpoint; actor deadlocked on a synchronous cross-actor call; heavy event loop under load.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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