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

  1. Treat this as a closed connection: the peer will not accept more data; tear down and restart the relay session
  2. Check the remote server/relay target health — premature closes upstream propagate as WriteZero here
  3. If you implemented a custom AsyncWrite target, ensure poll_write never returns Ok(0): return Poll::Pending when no progress is possible
  4. 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

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.