{"record":{"id":"828eabf9f9a19aa4","repo":"quickwit-oss/quickwit","slug":"body-stream-ended-after-skipping-copied-bytes-e","errorCode":null,"errorMessage":"body stream ended after skipping {copied} bytes; expected to skip {n} more","messagePattern":"body stream ended after skipping (.+?) bytes; expected to skip (.+?) more","errorType":"exception","errorClass":"io::Error (UnexpectedEof)","httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-parquet-engine/src/storage/streaming_reader.rs","lineNumber":604,"sourceCode":"    Ok(())\n}\n\n/// Discard `n` bytes by reading and dropping them. Bytes already in\n/// `pending` are drained first; remaining bytes are read from `body`.\nasync fn skip_forward(state: &mut ReadingState, mut n: usize) -> io::Result<()> {\n    if n == 0 {\n        return Ok(());\n    }\n    let from_pending = n.min(state.pending.len());\n    state.pending.drain(..from_pending);\n    n -= from_pending;\n    if n == 0 {\n        return Ok(());\n    }\n    let mut sink = tokio::io::sink();\n    let copied = tokio::io::copy(&mut (&mut state.body).take(n as u64), &mut sink).await?;\n    if copied < n as u64 {\n        return Err(io::Error::new(\n            io::ErrorKind::UnexpectedEof,\n            format!(\"body stream ended after skipping {copied} bytes; expected to skip {n} more\",),\n        ));\n    }\n    Ok(())\n}\n\n/// Compute the (start, end) offsets of the body byte range — first\n/// column chunk's start to last column chunk's end. Returns\n/// `(0, 0)` for files with zero row groups.\nfn compute_body_range(metadata: &ParquetMetaData) -> (u64, u64) {\n    let num_rgs = metadata.num_row_groups();\n    if num_rgs == 0 {\n        return (0, 0);\n    }\n    let first_rg = metadata.row_group(0);\n    if first_rg.num_columns() == 0 {\n        return (0, 0);","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-parquet-engine/src/storage/streaming_reader.rs#L586-L622","documentation":"`skip_forward` discards `n` bytes of the parquet stream by copying them into a sink. If the body yields fewer than `n` bytes before EOF, the reader cannot land on the next page boundary and returns UnexpectedEof from `next_page`.","triggerScenarios":"Calling next_page when the remote body ends early during a skip: truncated split file, connection dropped mid-skip, or a range request returning fewer bytes than requested.","commonSituations":"Same network reliability issues as read failures: LB idle timeouts, reset connections, object storage returning short reads for corrupted or concurrently-deleted objects.","solutions":["Retry the operation with a fresh connection/range request.","Validate the split's stored size and checksum; re-upload if the object is truncated.","Inspect intermediate proxies/timeouts if the failure is reproducible at a fixed byte offset.","If persistent, re-index or restore the split from a known-good copy."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_skip_eof(err: &std::io::Error) -> bool { err.kind() == std::io::ErrorKind::UnexpectedEof }","tryCatchPattern":"if let Err(e) = next_page().await {\n    if e.kind() == std::io::ErrorKind::UnexpectedEof {\n        // reopen a fresh connection/range request and retry\n        retry_with_backoff(|| reopen_and_next_page()).await?;\n    } else { return Err(e.into()); }\n}","preventionTips":["Validate split file size/checksum before serving queries from it.","Use bounded retries with fresh connections for long-lived body streams.","Watch for proxies terminating mid-transfer; configure keepalives."],"tags":["io","network","parquet","unexpected-eof","streaming"],"backgroundTag":"http-request-failed","analyzedSha":"a39730c5cdcd1a4fe798403737ae293999ea21f8","analyzedAt":"2026-09-08T13:19:37.784Z","contentChangedAt":"2026-09-08T13:19:37.784Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}