nautechsystems/nautilus_trader · error · anyhow::Error
Polymarket WebSocket handler did not stop after abort
Error message
Polymarket WebSocket handler did not stop after abort
What it means
disconnect() aborts the feed-handler task and waits up to 2 seconds for it to finish. TaskJoinOutcome::Incomplete means the task neither completed, failed, nor acknowledged the abort within that window — it is stuck (e.g. blocked in a non-cancellation-safe await). The disconnect call reports this as an error because a stale handler task may still hold the WebSocket connection and channel receivers.
Source
Thrown at crates/adapters/polymarket/src/websocket/client.rs:459
log::debug!("Disconnecting Polymarket WebSocket");
self.signal.store(true, Ordering::Relaxed);
if let Err(e) = self.cmd_tx.read().await.send(HandlerCommand::Disconnect) {
log::debug!("Failed to send disconnect (handler may already be shut down): {e}");
}
let task_result = match finish_task(
&mut self.task_handle,
std::time::Duration::from_secs(2),
std::time::Duration::from_secs(2),
)
.await
{
None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => Ok(()),
Some(TaskJoinOutcome::Failed(error)) => Err(anyhow::anyhow!(
"Polymarket WebSocket handler failed: {error}"
)),
Some(TaskJoinOutcome::Incomplete) => Err(anyhow::anyhow!(
"Polymarket WebSocket handler did not stop after abort"
)),
};
// Invalidate after the task has stopped so any in-flight auth_tracker.succeed()
// calls from the handler cannot race with and survive the invalidation.
self.auth_tracker.invalidate();
if let Some(control) = &self.socket_control {
control.deregister();
}
log::debug!("Polymarket WebSocket disconnected");
task_result
}
/// Returns `true` if the WebSocket is actively connected.
#[must_use]
pub fn is_active(&self) -> bool {
ConnectionMode::from_atomic(&self.connection_mode).is_active()View on GitHub (pinned to 18893faf8b)
Solutions
- Ensure all connect/read/write operations inside the handler use timeouts (tokio::time::timeout) so abort is honored promptly
- Retry disconnect() once — the stuck await may complete and let a second join succeed
- Avoid holding locks across .await points in the handler loop
- If a stale task remains, drop the whole client so the channel closes and the task unwinds eventually
Example fix
// before
client.disconnect().await?; // may fail: handler stuck >2s after abort
// after
if let Err(e) = client.disconnect().await {
log::warn!("handler did not stop in time, dropping client: {e}");
drop(client); // dropping closes channels and lets the task unwind
} Defensive patterns
Strategy: retry
Try / catch
if let Err(e) = client.disconnect().await {
if e.to_string().contains("did not stop after abort") {
tokio::time::sleep(Duration::from_millis(100)).await;
client.disconnect().await.ok(); // second join usually succeeds
}
} Prevention
- Ensure every await in the handler loop is cancellation-safe and timeout-bounded
- Never hold a mutex/lock across .await inside the handler
- Add connect/read timeouts so a hung TCP attempt cannot block abort
- Drop the whole client if a second disconnect also fails, letting channel closure unwind the task
When it happens
Trigger: Calling disconnect() while the handler task is blocked in an await point that ignores cancellation — e.g. a long blocking read, a lock held across awaits, or a reconnect loop stuck on TCP connect to an unresponsive host without timeouts.
Common situations: Network partition causing a connect attempt with no timeout to hang; handler waiting on a mutex starved by another task; very slow/broken gateway that stalls the read loop past the 2s join window.
Related errors
- Polymarket WebSocket handler failed: {error}
- Timeout waiting for account {account_id} to be registered af
- Timeout waiting for account {account_id} to be registered af
- Timeout waiting for account {account_id} to be registered af
- Lighter WebSocket handler did not stop after abort
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/4a08808697a028c7.
Report an issue: GitHub.