quickwit-oss/quickwit · error · io::Error (UnexpectedEof)

body stream ended after skipping {copied} bytes; expected to

Error message

body stream ended after skipping {copied} bytes; expected to skip {n} more

What it means

`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`.

Source

Thrown at quickwit/quickwit-parquet-engine/src/storage/streaming_reader.rs:604

    Ok(())
}

/// Discard `n` bytes by reading and dropping them. Bytes already in
/// `pending` are drained first; remaining bytes are read from `body`.
async fn skip_forward(state: &mut ReadingState, mut n: usize) -> io::Result<()> {
    if n == 0 {
        return Ok(());
    }
    let from_pending = n.min(state.pending.len());
    state.pending.drain(..from_pending);
    n -= from_pending;
    if n == 0 {
        return Ok(());
    }
    let mut sink = tokio::io::sink();
    let copied = tokio::io::copy(&mut (&mut state.body).take(n as u64), &mut sink).await?;
    if copied < n as u64 {
        return Err(io::Error::new(
            io::ErrorKind::UnexpectedEof,
            format!("body stream ended after skipping {copied} bytes; expected to skip {n} more",),
        ));
    }
    Ok(())
}

/// Compute the (start, end) offsets of the body byte range — first
/// column chunk's start to last column chunk's end. Returns
/// `(0, 0)` for files with zero row groups.
fn compute_body_range(metadata: &ParquetMetaData) -> (u64, u64) {
    let num_rgs = metadata.num_row_groups();
    if num_rgs == 0 {
        return (0, 0);
    }
    let first_rg = metadata.row_group(0);
    if first_rg.num_columns() == 0 {
        return (0, 0);

View on GitHub (pinned to a39730c5cd)

Solutions

  1. Retry the operation with a fresh connection/range request.
  2. Validate the split's stored size and checksum; re-upload if the object is truncated.
  3. Inspect intermediate proxies/timeouts if the failure is reproducible at a fixed byte offset.
  4. If persistent, re-index or restore the split from a known-good copy.
Defensive patterns

Strategy: retry

Type guard

fn is_skip_eof(err: &std::io::Error) -> bool { err.kind() == std::io::ErrorKind::UnexpectedEof }

Try / catch

if let Err(e) = next_page().await {
    if e.kind() == std::io::ErrorKind::UnexpectedEof {
        // reopen a fresh connection/range request and retry
        retry_with_backoff(|| reopen_and_next_page()).await?;
    } else { return Err(e.into()); }
}

Prevention

When it happens

Trigger: 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.

Common situations: Same network reliability issues as read failures: LB idle timeouts, reset connections, object storage returning short reads for corrupted or concurrently-deleted objects.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/828eabf9f9a19aa4. Report an issue: GitHub.