{"record":{"id":"ce8557a245d6b7ed","repo":"nautechsystems/nautilus_trader","slug":"invalid-topic-format-empty-topic","errorCode":null,"errorMessage":"Invalid topic format: empty topic","messagePattern":"Invalid topic format: empty topic","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/bybit/src/websocket/parse.rs","lineNumber":184,"sourceCode":"            return serde_json::from_value(value.clone()).map_or_else(\n                |_| BybitWsFrame::Unknown(value),\n                BybitWsFrame::AccountPosition,\n            );\n        }\n    }\n\n    BybitWsFrame::Unknown(value)\n}\n\n/// Parses a Bybit WebSocket topic string into its components.\n///\n/// # Errors\n///\n/// Returns an error if the topic format is invalid.\npub fn parse_topic(topic: &str) -> anyhow::Result<Vec<&str>> {\n    let parts: Vec<&str> = topic.split('.').collect();\n    if parts.is_empty() {\n        anyhow::bail!(\"Invalid topic format: empty topic\");\n    }\n    Ok(parts)\n}\n\n/// Parses a Bybit kline topic into (interval, symbol).\n///\n/// Topic format: \"kline.{interval}.{symbol}\" (e.g., \"kline.5.BTCUSDT\")\n///\n/// # Errors\n///\n/// Returns an error if the topic format is invalid.\npub fn parse_kline_topic(topic: &str) -> anyhow::Result<(&str, &str)> {\n    let kline = BybitWsPublicChannel::Kline.as_ref();\n    let parts = parse_topic(topic)?;\n    if parts.len() != 3 || parts[0] != kline {\n        anyhow::bail!(\n            \"Invalid kline topic format: expected '{kline}.{{interval}}.{{symbol}}', was '{topic}'\"\n        );","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/bybit/src/websocket/parse.rs#L166-L202","documentation":"`parse_topic` splits a Bybit WebSocket topic string on '.' and rejects input that produces no parts. In practice `str::split` never yields an empty vec, so this is a defensive guard against empty/invalid topic input reaching the kline parser.","triggerScenarios":"Calling `parse_topic` (or `parse_kline_topic`, its only caller) with an empty or malformed topic string, typically when `handle_ws_message` receives a message whose `topic` field is empty or missing.","commonSituations":"A Bybit public WebSocket message arrives with an empty `topic` field (e.g. handshake/ack frames, ping/pong payloads, or a malformed frame) and is fed into kline topic parsing.","solutions":["Filter incoming WS messages: only pass frames whose `topic` field is a non-empty string into `parse_kline_topic`.","Check that the message is actually a kline channel frame before parsing (match on topic prefix).","Log and skip unexpected frames (pong/ack) instead of routing them to topic parsing."],"exampleFix":"// before\nif let Some(topic) = msg.topic {\n    let (interval, symbol) = parse_kline_topic(&topic)?;\n}\n// after\nif let Some(topic) = msg.topic {\n    if !topic.is_empty() && topic.starts_with(\"kline\") {\n        let (interval, symbol) = parse_kline_topic(&topic)?;\n    }\n}","handlingStrategy":"validation","validationCode":"fn is_parseable_topic(topic: &str) -> bool {\n    !topic.is_empty()\n}","typeGuard":"fn non_empty_topic(topic: Option<&str>) -> Option<&str> {\n    topic.filter(|t| !t.is_empty())\n}","tryCatchPattern":"match parse_kline_topic(topic) {\n    Ok((interval, symbol)) => process_kline(interval, symbol, msg),\n    Err(e) => tracing::debug!(\"skipping frame with unusable topic: {e}\"),\n}","preventionTips":["Filter WS frames without a non-empty `topic` before parsing","Handle ping/pong/ack frames separately from data frames","Dispatch on topic prefix before delegating to specific parsers"],"tags":["bybit","websocket","parsing","topic"],"backgroundTag":"invalid-argument-format","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}