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

  1. Check database server logs for crashes/restarts around the time of the error
  2. Enable TCP keepalive / set idle timeouts so stale connections are detected before use
  3. Reconnect and retry the operation; wrap long-lived connections with a health check (e.g. `pool.acquire` + ping)
  4. 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

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


AI-assisted analysis of transact-rs/sqlx@03af8bcc57 (2026-09-03). Data as JSON: /api/errors/06003a629bf6d058. Report an issue: GitHub.