nautechsystems/nautilus_trader · error · anyhow::Error
Architect AX data WebSocket handler did not stop after abort
Error message
Architect AX data WebSocket handler did not stop after abort
What it means
Returned by the Architect AX data WebSocket client's close() when, after issuing an abort to the handler task, the join outcome is TaskJoinOutcome::Incomplete — meaning the task did not finish within the abort/join window.
Source
Thrown at crates/adapters/architect_ax/src/websocket/data/client.rs:1081
self.signal.store(true, Ordering::Release);
let outcome = self
.task_handle
.finish(Duration::from_secs(2), Duration::from_secs(2))
.await;
*self.reconnect_headers.lock() = None;
if let Some(control) = &self.socket_control {
control.deregister();
}
match outcome {
None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => Ok(()),
Some(TaskJoinOutcome::Failed(error)) => Err(anyhow::anyhow!(
"Architect AX data WebSocket handler failed: {error}"
)),
Some(TaskJoinOutcome::Incomplete) => Err(anyhow::anyhow!(
"Architect AX data WebSocket handler did not stop after abort"
)),
}
}
async fn send_cmd(&self, cmd: HandlerCommand) -> AxWsResult<()> {
let guard = self.cmd_tx.read().await;
guard
.send(cmd)
.map_err(|e| AxWsClientError::ChannelError(e.to_string()))
}
}
impl Drop for AxMdWebSocketClient {
fn drop(&mut self) {
if Arc::strong_count(&self.task_handle) == 1 && !self.task_handle.is_empty() {
self.cancellation_token.load().cancel();
self.signal.store(true, Ordering::Release);View on GitHub (pinned to 18893faf8b)
Solutions
- Check the handler loop for awaits or blocking calls that ignore the abort/cancellation token
- Increase the shutdown timeout if it is legitimately slow, or add cooperative cancellation checks in the handler loop
- Look for deadlock between handler and command channels (e.g. unbounded queue consumption ordering)
- Restart the client/connection; the task may be leaked and should be reported as a bug if reproducible
Example fix
// before
loop {
let msg = ws.next().await; // ignores cancellation
...
}
// after
loop {
tokio::select! {
_ = cancel_token.cancelled() => break,
msg = ws.next() => { ... }
}
} Defensive patterns
Strategy: try-catch
Try / catch
// Rust
match client.close().await {
Ok(()) => {},
Err(e) if e.to_string().contains("did not stop after abort") => {
tracing::error!("handler task leaked: {e}");
}
Err(e) => return Err(e),
} Prevention
- Use tokio::select! with the cancellation token in every long await in the handler
- Avoid blocking (std::thread::sleep, sync IO) inside async handler code
- Keep handler waits bounded with timeouts so tasks can always reach an exit point
- Test shutdown under slow-network conditions
When it happens
Trigger: Calling close() and the handler task neither completes, fails, nor acknowledges the abort in time (e.g. it is stuck in a long await, a blocking loop, or ignoring the abort signal).
Common situations: Handler blocked on a network read without honoring cancellation; a deadlock between the handler and command channel; slow/hung remote server keeping the task alive during shutdown.
Related errors
- Hyperliquid WebSocket handler did not stop after abort
- Failed to terminate Hyperliquid data session tasks: {e}
- Hyperliquid WebSocket handler failed: {error}
- Lighter WebSocket handler did not stop after abort
- Polymarket RTDS task shutdown failed: {}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/45c2de0d1e719825.
Report an issue: GitHub.