nautechsystems/nautilus_trader · error

Invalid kline topic format: expected '{kline}.{{interval}}.{

Error message

Invalid kline topic format: expected '{kline}.{{interval}}.{{symbol}}', was '{topic}'

What it means

`parse_kline_topic` expects a Bybit topic of exactly three dot-separated segments starting with `kline` (`kline.{interval}.{symbol}`). Any other segment count or prefix produces this error naming the expected shape and the actual topic.

Source

Thrown at crates/adapters/bybit/src/websocket/parse.rs:200

    let parts: Vec<&str> = topic.split('.').collect();
    if parts.is_empty() {
        anyhow::bail!("Invalid topic format: empty topic");
    }
    Ok(parts)
}

/// Parses a Bybit kline topic into (interval, symbol).
///
/// Topic format: "kline.{interval}.{symbol}" (e.g., "kline.5.BTCUSDT")
///
/// # Errors
///
/// Returns an error if the topic format is invalid.
pub fn parse_kline_topic(topic: &str) -> anyhow::Result<(&str, &str)> {
    let kline = BybitWsPublicChannel::Kline.as_ref();
    let parts = parse_topic(topic)?;
    if parts.len() != 3 || parts[0] != kline {
        anyhow::bail!(
            "Invalid kline topic format: expected '{kline}.{{interval}}.{{symbol}}', was '{topic}'"
        );
    }
    Ok((parts[1], parts[2]))
}

/// Parses a WebSocket trade frame into a [`TradeTick`].
pub fn parse_ws_trade_tick(
    trade: &BybitWsTrade,
    instrument: &InstrumentAny,
    ts_init: UnixNanos,
) -> anyhow::Result<TradeTick> {
    let price = parse_price_with_precision(&trade.p, instrument.price_precision(), "trade.p")?;
    let size = parse_quantity_with_precision(&trade.v, instrument.size_precision(), "trade.v")?;
    let aggressor: AggressorSide = trade.taker_side.into();
    let trade_id = TradeId::new_checked(trade.i.as_str())
        .context("invalid trade identifier in Bybit trade message")?;
    let ts_event = parse_millis_i64(trade.t, "trade.T")?;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Dispatch messages by topic prefix before calling `parse_kline_topic` (e.g. only topics starting with `kline.`).
  2. Verify the subscription topic matches Bybit v5 public kline format: `kline.{interval}.{symbol}`.
  3. Check for Bybit API version changes if topic formats shifted after an exchange update.

Example fix

// before: parse every message as kline
let (interval, symbol) = parse_kline_topic(&topic)?;
// after: route by channel first
if topic.starts_with(BybitWsPublicChannel::Kline.as_ref()) {
    let (interval, symbol) = parse_kline_topic(&topic)?;
}
Defensive patterns

Strategy: type-guard

Validate before calling

fn is_kline_topic(topic: &str) -> bool {
    let parts: Vec<&str> = topic.split('.').collect();
    parts.len() == 3 && parts[0] == "kline"
}

Type guard

fn as_kline_topic(topic: &str) -> Option<(&str, &str, &str)> {
    let parts: Vec<&str> = topic.split('.').collect();
    match parts.as_slice() {
        ["kline", interval, symbol] if !interval.is_empty() && !symbol.is_empty() => {
            Some(("kline", interval, symbol))
        }
        _ => None,
    }
}

Try / catch

if is_kline_topic(&topic) {
    let (interval, symbol) = parse_kline_topic(&topic)?;
} else {
    tracing::debug!("non-kline topic ignored: {topic}");
}

Prevention

When it happens

Trigger: `handle_ws_message` receives a public WS message whose topic is not a kline topic (e.g. `orderbook.1.BTCUSDT`, `publicTrade.BTCUSDT`, or `kline.1.BTCUSDT.extra`) and `parse_kline_topic` is called on it.

Common situations: Subscribing to multiple public channels but routing every message through the kline parser; Bybit renaming/adding channel topics in a new API version; typos in subscription topic strings.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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