{"record":{"id":"6938b55c9d30e1c3","repo":"vi/websocat","slug":"write-zero-byte-into-writer","errorCode":null,"errorMessage":"write zero byte into writer","messagePattern":"write zero byte into writer","errorType":"exception","errorClass":"io::Error (ErrorKind::WriteZero)","httpStatus":null,"severity":"error","filePath":"src/my_copy.rs","lineNumber":91,"sourceCode":"    }\n}\n\nimpl<R, W> Future for Copy<R, W>\nwhere\n    R: AsyncRead,\n    W: AsyncWrite,\n{\n    type Item = (u64, R, W);\n    type Error = io::Error;\n\n    fn poll(&mut self) -> Poll<(u64, R, W), io::Error> {\n        loop {\n            // First ensure that preamble messages got drained\n            if self.preamble_index < self.preamble.len() {\n                let writer = self.writer.as_mut().unwrap();\n                let i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));\n                if i == 0 {\n                    return Err(io::Error::new(\n                        io::ErrorKind::WriteZero,\n                        \"write zero byte into writer\",\n                    ));\n                } else {\n                    trace!(\"preamble write {}\", i);\n                    if i != self.preamble[self.preamble_index].len() {\n                        warn!(\"Short write of a preamble. Expect trimmed data.\")\n                    }\n                    self.preamble_index += 1;\n                }\n                try_nb!(writer.flush());\n                continue;\n            }\n\n            // Handle inhibiting options only after preamble is drained.\n            if self.opts.skip {\n                debug!(\"copy skipped\");\n                let reader = self.reader.take().unwrap();","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/vi/websocat/blob/3a3574cd2f5d17857d87f3982e72c3ede159dde0/src/my_copy.rs#L73-L109","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));\nif i == 0 {\n    return Err(io::Error::new(io::ErrorKind::WriteZero, \"write zero byte into writer\"));\n}\n// after\n// guard upstream: stop feeding the copy when the peer is half-closed\nif is_peer_closed(&self.socket) {\n    return Err(io::Error::new(io::ErrorKind::BrokenPipe, \"peer closed during preamble write\"));\n}\nlet i = try_nb!(writer.write(self.preamble[self.preamble_index].as_bytes()));\nif i == 0 {\n    return Err(io::Error::new(io::ErrorKind::WriteZero, \"write zero byte into writer\"));\n}","handlingStrategy":"try-catch","validationCode":"// check the sink is still writable before starting the copy\nif socket.is_closed() || socket.take_error()?.is_some() {\n    return Err(io::Error::new(io::ErrorKind::NotConnected, \"sink closed before copy\"));\n}","typeGuard":"fn sink_usable<W: AsyncWrite + Unpin>(w: &mut W) -> bool {\n    // a writer must never report Ok(0); only proceed on healthy sinks\n    !w.is_write_vectored() || true // placeholder: prefer explicit liveness check per sink type\n}","tryCatchPattern":"match copy_future.await {\n    Err(e) if e.kind() == io::ErrorKind::WriteZero => {\n        // sink made no progress: treat as peer closed, abort transfer\n        log::warn!(\"peer stopped accepting data: {}\", e);\n        shutdown_connection(&mut socket);\n    }\n    other => other?,\n}","preventionTips":["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."],"tags":["rust","io","write-zero","async"],"backgroundTag":"write-zero-bytes","analyzedSha":"3a3574cd2f5d17857d87f3982e72c3ede159dde0","analyzedAt":"2026-09-12T15:14:27.766Z","contentChangedAt":"2026-09-12T15:14:27.766Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}