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
- Check the peer/socket for closure before or after this error; treat WriteZero as a broken connection and drop the connection.
- Verify the underlying writer is a real AsyncWrite implementation, not one that returns Ok(0).
- Log and handle WriteZero in your poll loop as a terminal copy error rather than retrying.
- 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
- Never write to a writer you have already half-closed or shutdown.
- Ensure custom AsyncWrite impls return Pending/WouldBlock, never Ok(0).
- Monitor for peer disconnects (FIN/RST) and cancel copy futures promptly.
- Map WriteZero to a domain-level 'peer closed' error in application code.
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)