windmill-labs/windmill · error · ConversionError::Io (InvalidInput)
unexpected tuple byte `{}`
Error message
unexpected tuple byte `{}` What it means
In a logical replication Update/Delete message, each tuple is prefixed by a byte indicating its kind ('N' for new, etc.). The parser only accepts the known tuple bytes and raises InvalidInput 'unexpected tuple byte `{}`' otherwise. Like the other replication byte errors, it usually signals a malformed or misaligned message stream.
Source
Thrown at backend/windmill-trigger-postgres/src/replication_message.rs:375
TYPE_BYTE => {
buf.read_u32::<BigEndian>()?;
buf.read_cstr()?;
buf.read_cstr()?;
LogicalReplicationMessage::Type
}
INSERT_BYTE => {
let transaction_id = match logical_replication_settings.streaming {
true => Some(buf.read_i32::<BigEndian>()?),
false => None,
};
let o_id = buf.read_u32::<BigEndian>()?;
let byte = buf.read_u8()?;
let tuple = match byte {
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
byte => {
return Err(ConversionError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unexpected tuple byte `{}`", byte),
)));
}
};
LogicalReplicationMessage::Insert(InsertBody::new(transaction_id, o_id, tuple))
}
UPDATE_BYTE => {
let transaction_id = match logical_replication_settings.streaming {
true => Some(buf.read_i32::<BigEndian>()?),
false => None,
};
let o_id = buf.read_u32::<BigEndian>()?;
let byte = buf.read_u8()?;
let mut key_tuple = None;
let mut old_tuple = None;
View on GitHub (pinned to e474e8803c)
Solutions
- Restart replication / recreate the slot so parsing begins from a clean boundary.
- Identify the offending byte to check whether it's a valid-but-unsupported tuple kind and extend the parser if so.
- Confirm the pgoutput protocol options (proto version, publication) match the parser's expectations.
- Check for an earlier desynchronizing parse error in the logs and fix that first.
Defensive patterns
Strategy: try-catch
Try / catch
match parse(msg_bytes) {
Err(ConversionError::Io(e)) if e.to_string().contains("unexpected tuple byte") => {
tracing::warn!("unexpected tuple kind in update/delete message: {e}; resyncing slot");
}
Ok(msg) => handle(msg),
Err(e) => return Err(e.into()),
} Prevention
- Halt and resync the slot after the first parse error instead of continuing
- Match pgoutput proto version options to the parser's supported versions
- Log the offending byte to distinguish misalignment from genuinely new tuple kinds
- Test replication triggers against your exact Postgres version before production
When it happens
Trigger: Parsing an Update/Delete replication message where the byte after the tuple's relation OID/column count is not the expected tuple marker (e.g. TUPLE_NEW_BYTE) — from stream misalignment, protocol mismatch, or corruption.
Common situations: Continuing to parse after an earlier partial/failed message; a Postgres version emitting tuple kinds the parser doesn't handle; corrupted replication connections.
Related errors
- unknown replication message byte `{}`
- unknown replica identity byte `{}`
- unexpected EOF
- unknown tuple byte `{}`
- unknown tuple tag `{}`
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/6a85a21c73563084.
Report an issue: GitHub.