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

  1. Create a fresh `BytesMutWriter` for every payload; treat finalize as consuming the writer.
  2. Audit retry/resume logic: on retry after a finalized attempt, start a new writer rather than continuing.
  3. If the type exposes the flag, assert `!finalized` before writing as a debug guard.
  4. 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

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


AI-assisted analysis of rustfs/rustfs@9e6e02ea09 (2026-08-16). Data as JSON: /api/errors/b8f602f9d771d0cf. Report an issue: GitHub.