nautechsystems/nautilus_trader · error

Coinbase WebSocket handler did not stop after abort

Error message

Coinbase WebSocket handler did not stop after abort

What it means

When closing/reconnecting, connect() aborts the previous handler task and expects the task join to report Completed or Aborted. If the outcome is Incomplete (task neither finished nor confirmed aborted), the adapter treats this as an internal lifecycle violation and bails.

Source

Thrown at crates/adapters/coinbase/src/websocket/client.rs:241

        if self.is_active() || self.is_reconnecting() {
            log::warn!("WebSocket already connected or reconnecting");
            return Ok(());
        }

        if let Some(outcome) = finish_task(
            &mut self.task_handle,
            WS_DISCONNECT_TIMEOUT,
            WS_DISCONNECT_TIMEOUT,
        )
        .await
        {
            match outcome {
                TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => {}
                TaskJoinOutcome::Failed(error) => {
                    anyhow::bail!("Coinbase WebSocket handler failed: {error}");
                }
                TaskJoinOutcome::Incomplete => {
                    anyhow::bail!("Coinbase WebSocket handler did not stop after abort");
                }
            }
        }

        // Clear stop signal from any previous disconnect
        self.signal.store(false, Ordering::Relaxed);

        let (message_handler, raw_rx) = channel_message_handler();
        let cfg = WebSocketConfig {
            url: self.url.clone(),
            headers: vec![],
            // Coinbase uses TCP control-frame pings for transport keep-alive;
            // application-layer liveness comes from the heartbeats channel.
            heartbeat_interval_secs: Some(WS_HEARTBEAT_SECS),
            heartbeat_payload: None,
            connect_timeout_ms: Some(RECONNECT_TIMEOUT.as_millis() as u64),
            reconnect_delay_initial_ms: Some(RECONNECT_BASE_BACKOFF.as_millis() as u64),
            reconnect_delay_max_ms: Some(RECONNECT_MAX_BACKOFF.as_millis() as u64),

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Avoid concurrent connect()/disconnect() calls; serialize connection lifecycle on one task
  2. Retry the connect — this is usually a transient lifecycle race
  3. If persistent, inspect the handler loop to ensure it selects on the stop signal and report/patch the adapter
Defensive patterns

Strategy: retry

Try / catch

for attempt in 0..3 {
    match ws.connect().await {
        Ok(_) => break,
        Err(e) if e.to_string().contains("did not stop after abort") => {
            tokio::time::sleep(Duration::from_millis(200 * (attempt + 1))).await;
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: The handler task ignored the abort signal and the join result came back Incomplete — typically a stuck handler loop not honoring the stop signal or a join race during rapid disconnect/reconnect cycles.

Common situations: Handler blocked on a non-cancellation-safe await (e.g. a lock or a send that never completes) while disconnect() aborts it; calling connect() concurrently from multiple tasks.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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