{"record":{"id":"616ab3cb73718043","repo":"shadowsocks/shadowsocks-rust","slug":"write-zero-byte-into-writer","errorCode":null,"errorMessage":"write zero byte into writer","messagePattern":"write zero byte into writer","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/shadowsocks/src/relay/tcprelay/utils.rs","lineNumber":82,"sourceCode":"            if self.pos == self.cap && !self.read_done {\n                let me = &mut *self;\n                let mut buf = ReadBuf::new(&mut me.buf);\n                ready!(reader.as_mut().poll_read(cx, &mut buf))?;\n                let n = buf.filled().len();\n                if n == 0 {\n                    self.read_done = true;\n                } else {\n                    self.pos = 0;\n                    self.cap = n;\n                }\n            }\n\n            // If our buffer has some data, let's write it out!\n            while self.pos < self.cap {\n                let me = &mut *self;\n                let i = ready!(writer.as_mut().poll_write(cx, &me.buf[me.pos..me.cap]))?;\n                if i == 0 {\n                    return Poll::Ready(Err(io::Error::new(\n                        io::ErrorKind::WriteZero,\n                        \"write zero byte into writer\",\n                    )));\n                } else {\n                    self.pos += i;\n                    self.amt += i as u64;\n                }\n            }\n\n            // If we've written all the data and we've seen EOF, flush out the\n            // data and finish the transfer.\n            if self.pos == self.cap && self.read_done {\n                ready!(writer.as_mut().poll_flush(cx))?;\n                return Poll::Ready(Ok(self.amt));\n            }\n        }\n    }\n}","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/shadowsocks/shadowsocks-rust/blob/8eb0f0a65b1d976ab6bed5787327ef86529b0435/crates/shadowsocks/src/relay/tcprelay/utils.rs#L64-L100","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// custom AsyncWrite sink bug — before\nfn poll_write(...) { ... Ok(Ready(0)) }\n// after\nfn poll_write(...) {\n    if !self.has_capacity() { return Poll::Pending; } // never return Ok(0)\n    ...\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match relay_session.await {\n    Err(e) if e.kind() == std::io::ErrorKind::WriteZero => {\n        // peer writer closed; treat as normal disconnect\n        log::debug!(\"relay ended: writer accepted zero bytes (peer closed)\");\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","preventionTips":["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"],"tags":["io","write-zero","relay","stream","tcp"],"backgroundTag":"broken-pipe","analyzedSha":"8eb0f0a65b1d976ab6bed5787327ef86529b0435","analyzedAt":"2026-09-09T12:20:43.168Z","contentChangedAt":"2026-09-09T12:20:43.168Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}