tokio-rs/tokio · error · io::Error
failed to write frame to transport
Error message
failed to write frame to transport
What it means
Runtime error from the `Sink` `poll_flush` of `Framed`/`FramedWrite` (framed_impl.rs). While the output buffer is non-empty, `poll_write_buf` returned `Ok(0)` — the underlying writer accepted zero bytes. Tokio treats this as a `WriteZero` failure: the transport cannot accept the frame.
Source
Thrown at tokio-util/src/codec/framed_impl.rs:290
pinned
.codec
.encode(item, &mut pinned.state.borrow_mut().buffer)?;
Ok(())
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
use crate::util::poll_write_buf;
trace!("flushing framed transport");
let mut pinned = self.project();
while !pinned.state.borrow_mut().buffer.is_empty() {
let WriteFrame { buffer, .. } = pinned.state.borrow_mut();
trace!(remaining = buffer.len(), "writing;");
let n = ready!(poll_write_buf(pinned.inner.as_mut(), cx, buffer))?;
if n == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::WriteZero,
"failed to \
write frame to transport",
)
.into()));
}
}
// Try flushing the underlying IO
ready!(pinned.inner.poll_flush(cx))?;
trace!("framed transport flushed");
Poll::Ready(Ok(()))
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.as_mut().poll_flush(cx))?;
ready!(self.project().inner.poll_shutdown(cx))?;View on GitHub (pinned to 625954f365)
Solutions
- Treat the error as terminal for that connection: drop it and reconnect.
- Confirm you are not writing after shutting down the transport.
- Inspect `io::Error::kind()` (`WriteZero`) to distinguish a dead transport from transient backpressure.
- For custom `AsyncWrite` impls, never return `Ok(0)` unless genuinely EOF — return `Poll::Pending` instead.
Example fix
// before: ignoring sink errors
framed.send(item).await.unwrap();
// after: treat flush failure as a closed transport
if let Err(e) = framed.send(item).await {
if e.kind() == io::ErrorKind::WriteZero {
// transport is closed: reconnect or shut down this peer
reconnect().await;
} else {
return Err(e.into());
}
} Defensive patterns
Strategy: try-catch
Type guard
fn is_transport_dead(e: &io::Error) -> bool {
matches!(e.kind(), io::ErrorKind::WriteZero | io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset)
} Try / catch
if let Err(e) = framed.send(item).await {
if is_transport_dead(&e) { reconnect().await; continue; }
return Err(e.into());
} Prevention
- Never write after `poll_close`/`poll_shutdown` on the transport.
- Custom `AsyncWrite` impls should return `Poll::Pending`, not `Ok(0)`, when they cannot currently accept bytes.
- Model a zero-write as a terminal connection error in your reconnect logic.
When it happens
Trigger: The underlying `AsyncWrite` returned `0` from `write` without an error during a `Framed` sink flush — e.g. writing to a half-closed socket, a closed pipe, or a TLS session that has shut down the write side.
Common situations: Peer closed the connection while you were still sending (broken pipe / connection reset reported as a zero write); writing after `poll_close`/`poll_shutdown`; half-open TCP socket; a custom `AsyncWrite` that returns 0 spuriously.
Related errors
- bytes remaining on stream
- frame size too big
- failed to write entire datagram to socket
- provided length would overflow after adjustment
- Unable to decode input as UTF8
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/9a4439f261abf3b9.
Report an issue: GitHub.