transact-rs/sqlx · error · io::Error (UnexpectedEof)
expected to read {} bytes, got {} bytes at EOF
Error message
expected to read {} bytes, got {} bytes at EOF What it means
sqlx's buffered socket layer accumulates exactly `len` bytes before handing them to the protocol decoder. If the underlying socket returns 0 bytes (EOF) while the buffer still holds fewer than `len` bytes, it raises `UnexpectedEof` reporting how many bytes were expected versus received. This means the peer closed the connection mid-message.
Source
Thrown at sqlx-core/src/net/socket/buffered.rs:287
self.bytes_flushed = 0;
self.bytes_written = 0;
}
self.sanity_check();
}
}
impl ReadBuffer {
async fn read(&mut self, len: usize, socket: &mut impl Socket) -> io::Result<()> {
// Because of how `BytesMut` works, we should only be shifting capacity back and forth
// between `read` and `available` unless we have to read an oversize message.
while self.read.len() < len {
self.reserve(len - self.read.len());
let read = socket.read(&mut self.available).await?;
if read == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"expected to read {} bytes, got {} bytes at EOF",
len,
self.read.len()
),
));
}
self.advance(read);
}
Ok(())
}
fn reserve(&mut self, amt: usize) {
if let Some(additional) = amt.checked_sub(self.available.capacity()) {
self.available.reserve(additional);View on GitHub (pinned to 03af8bcc57)
Solutions
- Check database server logs for crashes/restarts around the time of the error
- Enable TCP keepalive / set idle timeouts so stale connections are detected before use
- Reconnect and retry the operation; wrap long-lived connections with a health check (e.g. `pool.acquire` + ping)
- Inspect proxies/firewalls/load balancers for idle connection limits
Example fix
// before: reusing a long-idle connection that the server already closed let row = conn.fetch_one(query).await?; // after: use a pool that validates connections before use let pool = PoolOptions::<MySql>::new().after_connect(|c| ...).test_before_acquire(true); let row = pool.acquire().await?.fetch_one(query).await?;
Defensive patterns
Strategy: retry
Try / catch
match conn.fetch_one(query).await {
Err(e) if e.as_database_error().map(|d| d.to_string().contains("EOF")).unwrap_or(false)
|| matches!(e.source(), Some(s) if s.to_string().contains("UnexpectedEof")) => {
// drop connection, reconnect via pool, retry once
}
other => other?,
} Prevention
- Use a connection pool with test_before_acquire and reasonable idle timeouts
- Enable TCP keepalive on long-lived connections
- Check DB server logs and LB/firewall idle-kill settings
- Wrap flaky reads in a bounded retry with backoff
When it happens
Trigger: Any `read` on a buffered socket where the remote side closes the TCP/UDS stream before `len` bytes of an in-flight message arrive — truncated server response, abrupt disconnect, proxy/load-balancer timeout cutting the stream.
Common situations: Database server restarted or crashed mid-query; connection idle-killed by a firewall or LB between client and DB; network interruption; reading past the end of a half-closed connection.
Related errors
- Hostname did not resolve to any addresses
- unimplemented!()
- Unix domain sockets are not supported on this platform
- absolute paths will only work on the current machine
- paths relative to the current file's directory are not curre
AI-assisted analysis of transact-rs/sqlx@03af8bcc57 (2026-09-03).
Data as JSON: /api/errors/06003a629bf6d058.
Report an issue: GitHub.