rustfs/rustfs · critical · io::Error
DARE stream truncated before a finalized package
Error message
DARE stream truncated before a finalized package
What it means
DecryptReader hit a clean EOF exactly at a DARE package boundary, but at least one package of the current part had already been decrypted (ref_nonce set) and none of them carried the final flag. DARE v2 streams must terminate with a final-flagged package; its absence means the encrypted object is truncated, so the reader returns UnexpectedEof. Zero decrypted packages is treated as a legitimately empty object and accepted.
Source
Thrown at crates/rio-v2/src/encrypt_reader.rs:473
}
while *this.header_read < DARE_HEADER_SIZE {
let mut read_buf = ReadBuf::new(&mut this.header_buf[*this.header_read..]);
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 {
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)),
}
}
View on GitHub (pinned to 9e6e02ea09)
Solutions
- Re-upload the object from a known-good copy — the plaintext is unrecoverable past the truncation point.
- If multipart, verify the complete-multipart part list matches the actually uploaded parts (order and content) before retrying.
- Check server logs around the original write for interrupted/crashed PUTs.
- Audit any tooling that reassembles or copies encrypted objects part-by-part; ensure it preserves the final package.
Defensive patterns
Strategy: try-catch
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("finalized package") => {
// Encrypted object truncated before its final package: unrecoverable — re-upload required.
integrity::mark_unrecoverable(&object_id);
Err(e)
}
Err(e) => Err(e),
} Prevention
- Verify multipart complete-multipart part lists against actually uploaded parts before committing encrypted objects.
- Ensure writers emit the final-flagged package before an object becomes visible to readers.
- Audit part-copy tooling to confirm it preserves the final DARE package.
When it happens
Trigger: An interrupted encrypted upload committed without the final package; multipart assembly using a wrong or stale part list so the tail part carrying the final package is missing; the object truncated after write; a range/assembly path that drops the last package.
Common situations: Crash during PUT of an SSE-encrypted object; manual xl.meta or multipart surgery; completing a multipart upload with an outdated parts list; mixed-version clusters during rolling upgrades mishandling the final flag.
Related errors
- unexpected EOF while reading DARE header
- unexpected EOF while reading DARE ciphertext
- DARE package nonce does not match the configured stream nonc
- DARE package nonce does not match the stream reference nonce
- UnexpectedEof
AI-assisted analysis of rustfs/rustfs@9e6e02ea09 (2026-08-16).
Data as JSON: /api/errors/d34390e13915acba.
Report an issue: GitHub.