rustfs/rustfs · error · std::io::Error
Cannot write to finalized writer
Error message
Cannot write to finalized writer
What it means
`BytesMutWriter` is a one-shot async writer: once `finalize()` flips the internal `finalized` flag, any further `poll_write` returns `WriteZero` with 'Cannot write to finalized writer'. The type enforces a single write session — data written after finalization would silently miss whatever the finalize step committed (checksums, trailers, fsync), so it refuses instead.
Source
Thrown at crates/io-core/src/writer.rs:265
impl std::fmt::Debug for BytesMutWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesMutWriter")
.field("buffer_len", &self.buffer.len())
.field("buffer_capacity", &self.buffer.capacity())
.field("bytes_written", &self.bytes_written)
.field("finalized", &self.finalized)
.finish()
}
}
/// AsyncWrite implementation for BytesMutWriter.
///
/// This allows the writer to be used with tokio's async I/O utilities.
impl AsyncWrite for BytesMutWriter {
fn poll_write(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, tokio::io::Error>> {
if self.finalized {
return Poll::Ready(Err(tokio::io::Error::new(
tokio::io::ErrorKind::WriteZero,
"Cannot write to finalized writer",
)));
}
let len = buf.len();
self.buffer.put_slice(buf);
self.bytes_written += len;
Poll::Ready(Ok(len))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), tokio::io::Error>> {
// Nothing to flush for in-memory buffer
Poll::Ready(Ok(()))
}
fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), tokio::io::Error>> {
self.finalized = true;View on GitHub (pinned to 9e6e02ea09)
Solutions
- Create a fresh `BytesMutWriter` for every payload; treat finalize as consuming the writer.
- Audit retry/resume logic: on retry after a finalized attempt, start a new writer rather than continuing.
- If the type exposes the flag, assert `!finalized` before writing as a debug guard.
- Restructure wrapper streams so nothing writes after they call finalize on the inner writer.
Example fix
// before: retry loop reuses a finalized writer
if let Err(e) = write_all(&mut writer, &data).await {
writer.finalize()?; // committed
writer.write_all(b"trailer").await?; // Err: Cannot write to finalized writer
}
// after: one writer per session
let mut writer = BytesMutWriter::new();
writer.write_all(&data).await?;
writer.write_all(b"trailer").await?;
writer.finalize()?; // finalize last, then drop Defensive patterns
Strategy: validation
Validate before calling
// One-shot discipline: finalize consumes the writer
async fn commit(w: &mut BytesMutWriter, data: &[u8]) -> io::Result<()> {
if w.is_finalized() { return Err(already_finalized()); } // if exposed
w.write_all(data).await?;
w.finalize()
} Type guard
// Model the session in types so writes-after-finalize cannot compile
enum Open; enum Finalized;
struct Writer<S> { _s: S }
// Writer<Open> has write()+finalize() -> Writer<Finalized>; Writer<Finalized> has neither Try / catch
// On WriteZero 'Cannot write to finalized writer': abort this attempt and
// restart with a fresh writer; never try to un-finalize.
if e.kind() == std::io::ErrorKind::WriteZero { writer = BytesMutWriter::new(); retry(); } Prevention
- Create a new writer per payload/retry attempt
- Treat finalize() as the last operation; drop the writer afterwards
- Audit wrapper streams for writes after they finalize the inner writer
When it happens
Trigger: Calling `write_all`/`poll_write` on a `BytesMutWriter` after `finalize()` succeeded: retry loops that resume writing after a completed attempt, a shared writer handed to a second producer after commit, or a duplex flow that keeps writing after shutdown/finalize was triggered.
Common situations: Request handlers pooling/reusing writers to avoid allocation; wrappers (e.g. compression or tar streams) that flush trailing bytes after the underlying writer was finalized; error paths that finalize early then bubble back into the write loop.
Related errors
- unexpected GetBucketVersioning XML namespace
- host is none
- invalid X-Amz-Content-Sha256 header value: {err}
- remote object version id is not valid ASCII
- remote tier returned an empty object version id header
AI-assisted analysis of rustfs/rustfs@9e6e02ea09 (2026-08-16).
Data as JSON: /api/errors/b8f602f9d771d0cf.
Report an issue: GitHub.