nautechsystems/nautilus_trader · error · TransportError::Io(std::io::Error)
Heartbeat interval cannot be zero
Error message
Heartbeat interval cannot be zero
What it means
The WebSocket client validates config invariants at construction. A heartbeat interval of exactly `Some(0)` seconds is rejected with `InvalidInput` because a zero-period heartbeat timer is meaningless (it would fire every loop iteration or never schedule correctly).
Source
Thrown at crates/network/src/websocket/client.rs:228
clippy::unused_async_trait_impl,
reason = "async signature for consistency with connect-based constructors"
)]
pub async fn new_with_writer(
config: WebSocketConfig,
writer: MessageWriter,
) -> Result<Self, TransportError> {
Self::new_with_writer_and_state_sink(config, writer, None)
}
fn new_with_writer_and_state_sink(
mut config: WebSocketConfig,
writer: MessageWriter,
state_sink: Option<SocketStateSink>,
) -> Result<Self, TransportError> {
install_cryptographic_provider();
if config.heartbeat_interval_secs == Some(0) {
return Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Heartbeat interval cannot be zero",
)));
}
let connection_mode = Arc::new(AtomicU8::new(ConnectionMode::Reconnect.as_u8()));
let connection_epoch = Arc::new(AtomicU64::new(0));
let state_notify = Arc::new(tokio::sync::Notify::new());
let controller_notify = Arc::new(tokio::sync::Notify::new());
let reconnect_published = Arc::new(AtomicBool::new(true));
let outcome =
ConnectionMode::complete_reconnect_with_sink(&connection_mode, state_sink.as_ref());
debug_assert_eq!(outcome, ReconnectOutcome::Reconnected);
// Note: We don't spawn a read task here since the reader is handled externally
let read_task = None;
let read_fence = None;
View on GitHub (pinned to 18893faf8b)
Solutions
- Use `None` to disable heartbeats instead of `Some(0)`.
- Set a positive interval such as `Some(15)` or `Some(30)` seconds.
- Clamp the value when loading from config: treat 0 as disabled.
Example fix
// before
let config = WebSocketConfig { heartbeat_interval_secs: Some(0), .. };
// after
let config = WebSocketConfig { heartbeat_interval_secs: Some(15), .. }; // or None to disable Defensive patterns
Strategy: validation
Validate before calling
fn sane_heartbeat(secs: Option<u64>) -> Option<u64> {
match secs { Some(0) | None => None, s => s } // 0 means disabled
}
// then: heartbeat_interval_secs: sane_heartbeat(raw_value) Try / catch
match WebSocketClient::new_with_writer_and_state_sink(...) {
Err(e) if e.to_string().contains("Heartbeat interval cannot be zero") => {
eprintln!("config error: use None to disable heartbeat");
return;
}
Err(e) => return Err(e),
Ok(c) => c,
} Prevention
- Represent 'disabled' as None, never Some(0).
- Clamp 0 from external config files to None at load time.
- Prefer the builder API so invariants are enforced early.
When it happens
Trigger: Building `WebSocketConfig` (often by struct literal, bypassing the builder) with `heartbeat_interval_secs: Some(0)` and passing it to `new_with_writer_and_state_sink`.
Common situations: Users intending 'no heartbeat' setting 0 instead of `None`, or computing the interval from a config value that defaults to 0 when unset.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- Not a subscription channel: {kind}
- {field} must be non-negative, was {value}
- order price must be in (0, 1)
- invalid order quantity
- Invalid WebSocket reconnect header name: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/60ff0520c44897b1.
Report an issue: GitHub.