windmill-labs/windmill · error · ConversionError::Io (UnexpectedEof)

unexpected EOF

Error message

unexpected EOF

What it means

`read_cstr` scans for the NUL terminator in the replication message buffer while parsing length-prefixed, null-terminated strings (e.g. relation names). If the buffer ends before a NUL byte is found, it returns UnexpectedEof 'unexpected EOF'. This means the message is truncated relative to its declared structure.

Source

Thrown at backend/windmill-trigger-postgres/src/replication_message.rs:264

impl Buffer {
    pub fn new(bytes: Bytes, idx: usize) -> Buffer {
        Buffer { bytes, idx }
    }

    fn slice(&self) -> &[u8] {
        &self.bytes[self.idx..]
    }

    fn read_cstr(&mut self) -> Result<String, ConversionError> {
        match self.slice().iter().position(|&x| x == 0) {
            Some(pos) => {
                let start = self.idx;
                let end = start + pos;
                let cstr = str::from_utf8(&self.bytes[start..end])?.to_owned();
                self.idx = end + 1;
                Ok(cstr)
            }
            None => Err(ConversionError::Io(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "unexpected EOF",
            ))),
        }
    }
}

impl Read for Buffer {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let len = {
            let slice = self.slice();
            let len = cmp::min(slice.len(), buf.len());
            buf[..len].copy_from_slice(&slice[..len]);
            len
        };
        self.idx += len;
        Ok(len)
    }

View on GitHub (pinned to e474e8803c)

Solutions

  1. Restart the replication stream / recreate the slot to get a clean, aligned message stream.
  2. Check for a prior parse error that desynchronized offsets — fix the root misparse.
  3. Verify the pgoutput protocol version options match the parser implementation.
  4. If reproducible, capture the offending message bytes and compare against the pgoutput spec to find the length mismatch.
Defensive patterns

Strategy: try-catch

Try / catch

match parse(msg_bytes) {
    Err(ConversionError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
        tracing::warn!("truncated replication message, resyncing: {e}");
        // drop & recreate the slot / restart the stream
    }
    Ok(msg) => handle(msg),
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: parse() calls read_cstr on a replication message whose bytes run out before the string's NUL terminator — typically when an earlier field was misread (wrong length) or the message was truncated in transport.

Common situations: Corrupted or truncated replication stream frames; a preceding parse step consuming the wrong number of bytes shifting all subsequent offsets; network layer delivering partial messages without the framing layer compensating.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/6de0ec2a391ad7b7. Report an issue: GitHub.