vi/websocat · error · io::Error (ErrorKind::WriteZero)

write zero byte into writer

Error message

write zero byte into writer

What it means

This io::Error with ErrorKind::WriteZero is raised when the async preamble-write loop inside poll() writes zero bytes into the underlying AsyncWriter. A zero-byte write from a writer that accepted the call signals the writer can no longer accept data (e.g. the remote end closed or the sink is a zero-capacity sink), and the copy protocol aborts instead of looping forever. The library throws it to prevent an infinite busy-loop when writer.write() makes no progress.

Solutions

  1. Check the peer/socket for closure before or after this error; treat WriteZero as a broken connection and drop the connection.
  2. Verify the underlying writer is a real AsyncWrite implementation, not one that returns Ok(0).
  3. Log and handle WriteZero in your poll loop as a terminal copy error rather than retrying.
  4. If using pipes, ensure the reader side stays open for the lifetime of the copy.

Example fix

// before
let i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));
if i == 0 {
    return Err(io::Error::new(io::ErrorKind::WriteZero, "write zero byte into writer"));
}
// after
// guard upstream: stop feeding the copy when the peer is half-closed
if is_peer_closed(&self.socket) {
    return Err(io::Error::new(io::ErrorKind::BrokenPipe, "peer closed during preamble write"));
}
let i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));
if i == 0 {
    return Err(io::Error::new(io::ErrorKind::WriteZero, "write zero byte into writer"));
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check the sink is still writable before starting the copy
if socket.is_closed() || socket.take_error()?.is_some() {
    return Err(io::Error::new(io::ErrorKind::NotConnected, "sink closed before copy"));
}

Type guard

fn sink_usable<W: AsyncWrite + Unpin>(w: &mut W) -> bool {
    // a writer must never report Ok(0); only proceed on healthy sinks
    !w.is_write_vectored() || true // placeholder: prefer explicit liveness check per sink type
}

Try / catch

match copy_future.await {
    Err(e) if e.kind() == io::ErrorKind::WriteZero => {
        // sink made no progress: treat as peer closed, abort transfer
        log::warn!("peer stopped accepting data: {}", e);
        shutdown_connection(&mut socket);
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling write (during the polled handshake phase) after the peer/sink has shut down: writer.write() returns Ok(0) for the current preamble message, so try_nb! succeeds but no bytes were written.

Common situations: Remote peer closed the connection mid-handshake; writing to a pipe/socket whose read side is gone; a custom AsyncWriter implementation that returns Ok(0) instead of Pending or an error; polling the future after shutdown without noticing it completed.

Related errors


AI-assisted analysis of vi/websocat@3a3574cd2f (2026-09-12). Data as JSON: /api/errors/6938b55c9d30e1c3. Report an issue: GitHub.

Appendix: source

Thrown at src/my_copy.rs:91

    }
}

impl<R, W> Future for Copy<R, W>
where
    R: AsyncRead,
    W: AsyncWrite,
{
    type Item = (u64, R, W);
    type Error = io::Error;

    fn poll(&mut self) -> Poll<(u64, R, W), io::Error> {
        loop {
            // First ensure that preamble messages got drained
            if self.preamble_index < self.preamble.len() {
                let writer = self.writer.as_mut().unwrap();
                let i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));
                if i == 0 {
                    return Err(io::Error::new(
                        io::ErrorKind::WriteZero,
                        "write zero byte into writer",
                    ));
                } else {
                    trace!("preamble write {}", i);
                    if i != self.preamble[self.preamble_index].len() {
                        warn!("Short write of a preamble. Expect trimmed data.")
                    }
                    self.preamble_index += 1;
                }
                try_nb!(writer.flush());
                continue;
            }

            // Handle inhibiting options only after preamble is drained.
            if self.opts.skip {
                debug!("copy skipped");
                let reader = self.reader.take().unwrap();

View on GitHub (pinned to 3a3574cd2f)