shadowsocks/shadowsocks-rust · error
write zero byte into writer
Error message
write zero byte into writer
What it means
During poll_copy (copying bytes between a shadowsocks stream and its peer), a writer's poll_write returned Ok(0), meaning it accepted zero bytes. Async writers must either make progress or return WouldBlock/Pending; Ok(0) signals the writer can never accept more data, so copy aborts with ErrorKind::WriteZero. This prevents an infinite busy-loop on a broken writer.
Source
Thrown at crates/shadowsocks/src/relay/tcprelay/utils.rs:82
if self.pos == self.cap && !self.read_done {
let me = &mut *self;
let mut buf = ReadBuf::new(&mut me.buf);
ready!(reader.as_mut().poll_read(cx, &mut buf))?;
let n = buf.filled().len();
if n == 0 {
self.read_done = true;
} else {
self.pos = 0;
self.cap = n;
}
}
// If our buffer has some data, let's write it out!
while self.pos < self.cap {
let me = &mut *self;
let i = ready!(writer.as_mut().poll_write(cx, &me.buf[me.pos..me.cap]))?;
if i == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"write zero byte into writer",
)));
} else {
self.pos += i;
self.amt += i as u64;
}
}
// If we've written all the data and we've seen EOF, flush out the
// data and finish the transfer.
if self.pos == self.cap && self.read_done {
ready!(writer.as_mut().poll_flush(cx))?;
return Poll::Ready(Ok(self.amt));
}
}
}
}View on GitHub (pinned to 8eb0f0a65b)
Solutions
- Treat this as a closed connection: the peer will not accept more data; tear down and restart the relay session
- Check the remote server/relay target health — premature closes upstream propagate as WriteZero here
- If you implemented a custom AsyncWrite target, ensure poll_write never returns Ok(0): return Poll::Pending when no progress is possible
- Add logging around the session to identify which side closed first and why
Example fix
// custom AsyncWrite sink bug — before
fn poll_write(...) { ... Ok(Ready(0)) }
// after
fn poll_write(...) {
if !self.has_capacity() { return Poll::Pending; } // never return Ok(0)
...
} Defensive patterns
Strategy: try-catch
Try / catch
match relay_session.await {
Err(e) if e.kind() == std::io::ErrorKind::WriteZero => {
// peer writer closed; treat as normal disconnect
log::debug!("relay ended: writer accepted zero bytes (peer closed)");
}
Err(e) => return Err(e),
Ok(()) => {}
} Prevention
- Treat WriteZero on a relay session as a closed connection, not a fatal bug
- Ensure upstream servers (and DNS targets) are healthy — premature closes surface here
- If providing custom AsyncWrite sinks, never return Ok(0) from poll_write; use Poll::Pending when blocked
- Keep OS socket buffers and half-close handling defaults unless you fully understand shutdown semantics
When it happens
Trigger: The underlying sink (TcpStream, UdpSocket/directed writer, or a wrapped writer) returns 0 from poll_write — typically because the peer closed the connection / socket is shut down for writing while data remains in the copy buffer.
Common situations: Remote server abruptly closed the relayed TCP connection mid-transfer; half-closed socket in the middle of a proxy session; custom stream implementation misreporting writes; UDP relay target gone.
Related errors
AI-assisted analysis of shadowsocks/shadowsocks-rust@8eb0f0a65b (2026-09-09).
Data as JSON: /api/errors/616ab3cb73718043.
Report an issue: GitHub.