rustfs/rustfs · error · io::Error
unexpected EOF while reading DARE ciphertext
Error message
unexpected EOF while reading DARE ciphertext
What it means
While DecryptReader fills ciphertext_buf for the current package, the underlying AsyncRead returned Ok with 0 filled bytes (a clean EOF) before the full package body (payload + 16-byte tag) arrived. The stream ends in the middle of a package whose header promised more bytes. It differs from 'DARE stream truncated before a finalized package', which fires at a package boundary when no final-flagged package was ever seen.
Source
Thrown at crates/rio-v2/src/encrypt_reader.rs:526
"non-final DARE package must carry a full 64KiB payload: cipher={}, payload_len={}, sequence_number={}, header={:02x?}",
header[1], payload_len, *this.sequence_number, header
),
)));
}
if this.ciphertext_buf.len() < package_len {
this.ciphertext_buf.resize(package_len, 0);
}
*this.ciphertext_len = package_len;
*this.ciphertext_read = 0;
while *this.ciphertext_read < *this.ciphertext_len {
let mut read_buf = ReadBuf::new(&mut this.ciphertext_buf[*this.ciphertext_read..*this.ciphertext_len]);
match this.inner.as_mut().poll_read(cx, &mut read_buf) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Ok(())) => {
let n = read_buf.filled().len();
if n == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"unexpected EOF while reading DARE ciphertext",
)));
}
*this.ciphertext_read += n;
}
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
}
}
match open_dare_package(
*this.key,
this.cipher,
*this.sequence_number,
*this.expected_base_nonce,
*header,
&this.ciphertext_buf[..*this.ciphertext_len],
this.ref_nonce,View on GitHub (pinned to 9e6e02ea09)
Solutions
- Compare the bytes the inner reader actually delivered against the object size in metadata; a mismatch means the backend view is stale or truncated — re-resolve the object and open a fresh stream.
- If the cut is transient (connection drop), resume: re-open the reader from the last full package boundary (sequence_number = ciphertext_offset / 65552); decrypted bytes before the cut remain valid.
- For uploads you control, ensure the encrypting writer flushes the final package before the object is completed.
- If truncation persists across re-reads, investigate backend/disk health rather than retrying.
Example fix
// before: a cut connection kills the whole read
let mut out = Vec::new();
reader.read_to_end(&mut out).await?;
// after: on UnexpectedEof, reopen and resume from the last full package boundary
match reader.read_to_end(&mut out).await {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
let seq = (ciphertext_pos / DARE_PACKAGE_SIZE) as u32;
reader = DecryptReader::new_with_sequence(reopen_from(seq), key, nonce, seq);
// continue reading; the decrypted prefix stays valid
}
result => result?,
} Defensive patterns
Strategy: retry
Validate before calling
// Before decrypting, confirm the stream can plausibly hold the framed packages
let min_len = full_packages * 65_552 + if has_final { 16 + last_payload + 16 } else { 0 };
if actual_object_len < min_len {
return Err(truncated_metadata(actual_object_len, min_len));
} Try / catch
match reader.read(&mut buf).await {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
// one bounded retry from the last full package boundary, then fail
let seq = (ciphertext_pos / DARE_PACKAGE_SIZE) as u32;
retry_once_with(DecryptReader::new_with_sequence(reopen(), key, nonce, seq)).await
}
other => other,
} Prevention
- Validate object size against metadata before opening the decrypt reader.
- Ensure uploads complete the final DARE package (EncryptReader finalizes on inner EOF) before the object is committed.
- For network bodies, check Content-Length against bytes actually received before handing the stream to DecryptReader.
When it happens
Trigger: Object content shorter than its recorded size (truncated upload, incomplete multipart complete); a network layer cutting the body early while the reader still expects payload + tag bytes; an inner range reader whose byte range ends before the current package completes; a writer that never flushed the trailing package.
Common situations: Proxy, CDN, or load balancer truncating GET responses; partial writes after disk-full; missing erasure-coded shards yielding short reads; metadata size disagreeing with actual object length after a failed overwrite.
Related errors
- DARE stream truncated before a finalized package
- unexpected EOF while reading DARE header
- non-final DARE package must carry a full 64KiB payload: ciph
- DARE payload too large for Version 2.0 framing
- DARE package cipher suite does not match the stream referenc
AI-assisted analysis of rustfs/rustfs@9e6e02ea09 (2026-08-16).
Data as JSON: /api/errors/521e67b35c7e9bc6.
Report an issue: GitHub.