nautechsystems/nautilus_trader · error · anyhow::Error

Failed to acquire WebSocket handler task spawner: {e}

Error message

Failed to acquire WebSocket handler task spawner: {e}

What it means

In OKX WebSocket `connect`, after starting a new handler generation, the client acquires a spawner used to launch per-stream handler tasks. If `spawner()` fails (task group not in a spawnable state), connect fails with this error before opening the connection.

Source

Thrown at crates/adapters/okx/src/websocket/client.rs:614

        let connect_lock = Arc::clone(&self.connect_lock);
        let _connect_guard = connect_lock.lock().await;

        if !self.handler_tasks.is_empty() && !self.handler_tasks.all_finished() {
            anyhow::bail!("Cannot connect while previous WebSocket handler task is still running");
        }

        if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
            self.handler_tasks.begin_shutdown();
            self.handler_tasks
                .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
                .await
                .map_err(|e| anyhow::anyhow!("Previous WebSocket handler failed: {e}"))?;
            self.handler_tasks.start_generation().map_err(|e| {
                anyhow::anyhow!("Failed to start WebSocket handler task generation: {e}")
            })?;
        }
        let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
            anyhow::anyhow!("Failed to acquire WebSocket handler task spawner: {e}")
        })?;
        let handler_abort = CancellationToken::new();
        *self.handler_abort.lock() = handler_abort.clone();
        let mut rollback = ConnectRollback {
            handler_tasks: Arc::clone(&self.handler_tasks),
            signal: Arc::clone(&self.signal),
            handler_abort: handler_abort.clone(),
            socket_control: self.socket_control.clone(),
            armed: true,
        };

        // Reset signal so is_active()/is_closed() work after a previous close()
        self.signal.store(false, Ordering::Release);

        let (message_handler, raw_rx) = channel_message_handler();

        // No-op ping handler: handler owns the WebSocketClient and responds to pings directly
        // in the message loop for minimal latency (see handler.rs TEXT_PONG response)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the client is not closed/shutting down before calling connect.
  2. Avoid concurrent connect calls on the same client; use a single supervisor task that owns the connection lifecycle.
  3. Retry connect with backoff; the transient task-group state usually resolves after the prior generation finishes.
Defensive patterns

Strategy: retry

Try / catch

// rust
if let Err(e) = client.connect().await {
    if e.to_string().contains("handler task spawner") {
        tokio::time::sleep(Duration::from_secs(1)).await;
        client.connect().await?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: connect called when `handler_tasks.spawner()` returns Err — the task group was not successfully started (e.g. generation start failed silently, group is closed/shutting down, or acquired concurrently by another connect).

Common situations: Reconnect storms sharing a single client instance; calling connect on a client whose close() has already begun; race between connect and disconnect on different tasks.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/826bdd1a9a2d611f. Report an issue: GitHub.