nautechsystems/nautilus_trader · error · TransportError

Cannot configure both ping_handler and epoch_ping_handler

Error message

Cannot configure both ping_handler and epoch_ping_handler

What it means

The WebSocketClient builder accepts two mutually exclusive incoming-ping mechanisms: a plain ping_handler and an epoch_ping_handler (which receives the connection epoch). Supplying both is an invalid configuration, rejected at build time with this InvalidInput TransportError before any connection is made. The client cannot represent both handler styles simultaneously, so the configuration is refused early.

Source

Thrown at crates/network/src/websocket/client.rs:2817

        finish_fn = connect
    )]
    pub async fn epoch_builder(
        config: WebSocketConfig,
        epoch_handler: EpochMessageHandler,
        ping_handler: Option<PingHandler>,
        epoch_ping_handler: Option<EpochPingHandler>,
        #[builder(default)] keyed_quotas: Vec<(String, Quota)>,
        default_quota: Option<Quota>,
        rate_limiter: Option<Arc<RateLimiter<Ustr, MonotonicClock>>>,
        state_sink: Option<SocketStateSink>,
        connection_rate_limiter: Option<Arc<RateLimiter<Ustr, MonotonicClock>>>,
        #[builder(default)] connection_rate_keys: Arc<[Ustr]>,
        initial_connect_retry_policy: Option<InitialConnectRetryPolicy>,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<Self, TransportError> {
        let ping_handler = match (ping_handler, epoch_ping_handler) {
            (Some(_), Some(_)) => {
                return Err(TransportError::Io(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    "Cannot configure both ping_handler and epoch_ping_handler",
                )));
            }
            (Some(handler), None) => Some(IncomingPingHandler::Ping(handler)),
            (None, Some(handler)) => Some(IncomingPingHandler::Epoch(handler)),
            (None, None) => None,
        };
        let rate_limiter = Self::resolve_rate_limiter(default_quota, keyed_quotas, rate_limiter)?;
        let connection_rate_limit =
            Self::resolve_connection_rate_limit(connection_rate_limiter, connection_rate_keys)?;
        Self::connect_with_handler_scoped(
            config,
            IncomingHandler::Epoch(epoch_handler),
            ping_handler,
            rate_limiter,
            state_sink,
            connection_rate_limit,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Remove the legacy ping_handler and keep only epoch_ping_handler if you need epoch context
  2. Keep only ping_handler if epoch tracking is unnecessary
  3. Centralize handler selection in one config path so only one is ever populated
  4. At your config layer, treat the two fields as mutually exclusive and error early with a clear message

Example fix

// before
WebSocketClient::new(..., Some(ping_handler), Some(epoch_ping_handler), ...)?
// after
WebSocketClient::new(..., None, Some(epoch_ping_handler), ...)?
Defensive patterns

Strategy: validation

Validate before calling

if ping_handler.is_some() && epoch_ping_handler.is_some() {
    return Err(anyhow!("only one of ping_handler / epoch_ping_handler may be set"));
}

Try / catch

match WebSocketClient::new(..., ping_handler, epoch_ping_handler, ...) {
    Err(e) if e.to_string().contains("both ping_handler") => {
        // fix config: drop the legacy handler and retry construction
    }
    r => r?,
}

Prevention

When it happens

Trigger: Calling WebSocketClient::new (builder) with Some(...) for both ping_handler and epoch_ping_handler parameters.

Common situations: Migrating code from the legacy ping_handler to epoch_ping_handler but leaving both set in config; a shared config layer that populates both from different sources; copy-paste wiring that forwards the existing handler plus a new epoch-aware one.

Related errors


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