{"record":{"id":"6b524c77cfba7bee","repo":"nautechsystems/nautilus_trader","slug":"empty-lighter-websocket-order-book-update","errorCode":null,"errorMessage":"empty Lighter WebSocket order book update","messagePattern":"empty Lighter WebSocket order book update","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/adapters/lighter/src/websocket/parse.rs","lineNumber":118,"sourceCode":"/// `is_snapshot` must be supplied by the caller because Lighter sends\n/// `subscribed/order_book` for the full book on subscription and\n/// `update/order_book` for incremental level changes afterwards.\n///\n/// # Errors\n///\n/// Returns an error if any price or size cannot be converted.\npub fn parse_ws_order_book_deltas(\n    book: &LighterWsOrderBook,\n    instrument: &InstrumentAny,\n    timestamp_ms: u64,\n    is_snapshot: bool,\n    ts_init: UnixNanos,\n) -> anyhow::Result<OrderBookDeltas> {\n    let ts_event = parse_millis_to_nanos(timestamp_ms)?;\n    let sequence = u64::try_from(book.nonce).context(\"negative Lighter book nonce\")?;\n    let total_levels = book.bids.len() + book.asks.len();\n\n    anyhow::ensure!(\n        is_snapshot || total_levels > 0,\n        \"empty Lighter WebSocket order book update\",\n    );\n\n    let mut deltas = Vec::with_capacity(total_levels + usize::from(is_snapshot));\n\n    if is_snapshot {\n        let mut clear = OrderBookDelta::clear(instrument.id(), sequence, ts_event, ts_init);\n        if total_levels == 0 {\n            clear.flags |= RecordFlag::F_LAST as u8;\n        }\n        deltas.push(clear);\n    }\n\n    let mut processed = 0_usize;\n\n    for bid in &book.bids {\n        processed += 1;","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/lighter/src/websocket/parse.rs#L100-L136","documentation":"parse_ws_order_book_deltas rejects order book WebSocket updates that contain no bid or ask levels (unless the update is a snapshot). The adapter treats an empty incremental update as a protocol/data anomaly rather than emitting an empty deltas batch.","triggerScenarios":"A book update message arrives with empty bids and asks arrays and is_snapshot is false, e.g. venue sends a keep-alive-like update or an unexpected payload shape.","commonSituations":"Venue protocol change or edge-case message (book emptied via separate delete events); network issues producing truncated payloads; subscribing to a book that has been delisted.","solutions":["Skip/ignore empty updates upstream if they are benign keep-alives","Log the raw message and check whether the venue changed the update schema","Handle the anyhow error by filtering it out in the message handler instead of failing the stream","Verify subscription is still valid and the market is active"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if !is_snapshot && book.bids.is_empty() && book.asks.is_empty() {\n    return Ok(()); // skip empty update\n}","typeGuard":"fn has_levels(book: &WsBook) -> bool { !book.bids.is_empty() || !book.asks.is_empty() }","tryCatchPattern":"match parse_ws_order_book_deltas(msg) {\n    Err(e) if e.to_string().contains(\"empty Lighter WebSocket order book update\") => {\n        log::debug!(\"skipping empty book update\");\n    }\n    other => other?,\n}","preventionTips":["Treat empty updates as benign and skip them in the handler","Monitor venue schema changes","Check book subscription health if empty updates cluster"],"tags":["rust","websocket","order-book","data-quality"],"backgroundTag":"empty-required-field","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"}