rustfs/rustfs · error · io::Error
unexpected EOF while reading DARE header
Error message
unexpected EOF while reading DARE header
What it means
DecryptReader received EOF from the inner reader after reading 1-15 of the 16 DARE package header bytes. Unlike the clean-boundary case, this is a mid-header cutoff: the stream ends a few bytes into a frame, so framing cannot complete and the reader returns UnexpectedEof. It indicates the ciphertext is short by less than one header, or the read window ended inside a header.
Source
Thrown at crates/rio-v2/src/encrypt_reader.rs:481
if n == 0 {
if *this.header_read == 0 {
// Clean EOF at a package boundary. Execution only reaches here
// with `finalized == false` (the finalized case is consumed at the
// loop top). If at least one package of the current part has been
// decrypted (`ref_nonce.is_some()`) but we never saw a final-flagged
// package, the final package is missing => DARE truncation. Zero
// decrypted packages (`ref_nonce.is_none()`) is a legitimately empty
// object (encrypt emits no packages for empty plaintext), so accept.
if this.ref_nonce.is_some() {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"DARE stream truncated before a finalized package",
)));
}
*this.finished = true;
return Poll::Ready(Ok(()));
}
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"unexpected EOF while reading DARE header",
)));
}
*this.header_read += n;
}
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
}
}
let header = this.header_buf;
if header[0] != DARE_VERSION_20 {
return Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData, "unsupported DARE version")));
}
if !matches!(header[1], DARE_CIPHER_AES_256_GCM | DARE_CIPHER_CHACHA20_POLY1305) {
return Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData, "unsupported DARE cipher suite")));
}
let payload_len = usize::from(u16::from_le_bytes([header[2], header[3]])) + 1;View on GitHub (pinned to 9e6e02ea09)
Solutions
- Verify object size against metadata (the encrypted size must be a valid DARE stream length) and heal or re-upload.
- When assembling encrypted objects from parts, align reads to package boundaries derived from the stream, not arbitrary offsets.
- Check the inner reader's source for early termination (Content-Length mismatch, connection cut).
Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the encrypted stream length is consistent before decrypting
if stored_size % DARE_PACKAGE_ALIGNMENT == 0 || stored_size < DARE_HEADER_SIZE {
return Err(marker_error("encrypted object size is not a valid DARE stream"));
} Try / catch
match r.read_buf(&mut buf).await {
Ok(n) => Ok(n),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof && e.to_string().contains("DARE header") => {
// Stream ends inside a 16-byte header: truncated object or misaligned range — fail, do not skip.
Err(e)
}
Err(e) => Err(e),
} Prevention
- Slice encrypted objects only at package boundaries derived from the DARE stream, never at raw offsets.
- Check Content-Length/size metadata consistency before starting decryption.
- Ensure writes of encrypted objects are atomic/committed before visibility.
When it happens
Trigger: Object truncated mid-header by a few bytes (short write, damaged tail shard), or a byte range/assembly that stops partway into a 16-byte DARE header instead of at a package boundary.
Common situations: Interrupted writes leaving a ragged tail; erasure reassembly short by a few bytes; custom range readers slicing encrypted objects at arbitrary offsets.
Related errors
- DARE stream truncated before a finalized package
- unexpected EOF while reading DARE ciphertext
- UnexpectedEof
- invalid AES-GCM key: {err}
- invalid ChaCha20-Poly1305 key: {err}
AI-assisted analysis of rustfs/rustfs@9e6e02ea09 (2026-08-16).
Data as JSON: /api/errors/75d1b8bf49603fa5.
Report an issue: GitHub.