windmill-labs/windmill · error · ConversionError::Io (InvalidInput)
unknown tuple byte `{}`
Error message
unknown tuple byte `{}` What it means
While parsing a logical-replication UPDATE ('U') message, the first tuple-type byte after the relation OID must be 'N' (new tuple), 'O' (old tuple, REPLICA IDENTITY FULL) or 'K' (key tuple). This error fires when that byte is none of those, so the UPDATE payload cannot be decoded under the pgoutput protocol rules. Like its siblings it is an InvalidInput io::Error wrapped in ConversionError and aborts decoding of the current message.
Source
Thrown at backend/windmill-trigger-postgres/src/replication_message.rs:413
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
TUPLE_OLD_BYTE | TUPLE_KEY_BYTE => {
if byte == TUPLE_OLD_BYTE {
old_tuple = Some(TupleData::parse(&mut buf)?);
} else {
key_tuple = Some(TupleData::parse(&mut buf)?);
}
match buf.read_u8()? {
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
byte => {
return Err(ConversionError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unexpected tuple byte `{}`", byte),
)));
}
}
}
byte => {
return Err(ConversionError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown tuple byte `{}`", byte),
)));
}
};
LogicalReplicationMessage::Update(UpdateBody::new(
transaction_id,
o_id,
old_tuple,
key_tuple,
new_tuple,
))
}
DELETE_BYTE => {
let transaction_id = match logical_replication_settings.streaming {
true => Some(buf.read_i32::<BigEndian>()?),
false => None,View on GitHub (pinned to e474e8803c)
Solutions
- Dump the raw frame around the failure and check the tuple marker byte: pgoutput UPDATE must carry 'O'/'K' (optional, per replica identity) followed by 'N'.
- Recreate the replication slot to resume from a clean WAL position if the stream is suspected corrupted (pg_drop_replication_slot, then create a new logical slot with pgoutput).
- Verify the source is real PostgreSQL with the pgoutput plugin, not another decoder or an intermediary that reformats messages.
- Fix test fixtures / fuzz inputs so the tuple byte is one of 'N', 'O', 'K'.
- If you need support for additional tuple markers, extend the match in replication_message.rs — but confirm the byte is actually valid pgoutput first.
Example fix
// hand-built UPDATE payload let mut buf = [b'U', 0, 0, 0, 42, b'Q', /* tuple data */]; // before: 'Q' is invalid let mut buf = [b'U', 0, 0, 0, 42, b'K', /* key tuple */ b'N', /* new tuple */]; // after
Defensive patterns
Strategy: try-catch
Validate before calling
// check UPDATE frame's tuple markers before parsing
fn update_marker_ok(payload: &[u8], replica_identity_full: bool) -> bool {
payload.len() >= 6
&& payload[0] == b'U'
&& matches!(payload[5], b'N' | b'K')
&& (!replica_identity_full || payload[5] != b'N' || true) // 'O' allowed for FULL
} Type guard
fn is_conversion_error(err: &std::io::Error) -> bool {
err.kind() == std::io::ErrorKind::InvalidInput
} Try / catch
match LogicalReplicationMessage::parse(buf, settings) {
Ok(LogicalReplicationMessage::Update(u)) => handle_update(u),
Ok(msg) => handle(msg),
Err(e) if e.to_string().contains("unknown tuple byte") => {
log::warn!("corrupt UPDATE frame, skipping: {e}");
}
Err(e) => return Err(e.into()),
} Prevention
- Know your replica identity: FULL yields 'O' old tuples, DEFAULT/INDEX yield 'K' key tuples — set it deliberately per table
- Sanity-check captured fixtures against the pgoutput message layout before feeding them to the parser
- Recreate the replication slot when decode errors repeat, to rule out a corrupted WAL stream
- Keep a raw-frame logger enabled at warn level so the offending byte is diagnosable
When it happens
Trigger: Calling LogicalReplicationMessage::parse on an XLogData payload whose first byte is UPDATE_BYTE ('U') and whose tuple-type byte after the relation OID is not 'N', 'O', or 'K' (e.g. 'D', 0x00, garbage).
Common situations: Corrupted/truncated WAL frames misaligning the reader; decoding a stream produced by a different or patched logical-decoding plugin; testing parse() with hand-built byte buffers where the marker byte was typoed; upstream PostgreSQL version protocol changes not accounted for in a hand-rolled client.
Related errors
- unknown tuple tag `{}`
- unknown replication message tag `{}`
- unknown replication message byte `{}`
- unexpected EOF
- unknown replica identity byte `{}`
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/8c262ec649e24927.
Report an issue: GitHub.