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

unknown tuple tag `{}`

Error message

unknown tuple tag `{}`

What it means

While parsing a logical-replication DELETE ('D') message, the tuple tag after the relation OID must be either 'K' (key tuple, default replica identity) or 'O' (old tuple, REPLICA IDENTITY FULL). This error fires for any other tag byte, meaning the delete payload violates the pgoutput wire format. The message is dropped from decoding and the error propagates as ConversionError (InvalidInput io::Error).

Source

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

                    new_tuple,
                ))
            }
            DELETE_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 tag = buf.read_u8()?;

                let mut key_tuple = None;
                let mut old_tuple = None;

                match tag {
                    TUPLE_OLD_BYTE => old_tuple = Some(TupleData::parse(&mut buf)?),
                    TUPLE_KEY_BYTE => key_tuple = Some(TupleData::parse(&mut buf)?),
                    tag => {
                        return Err(ConversionError::Io(io::Error::new(
                            io::ErrorKind::InvalidInput,
                            format!("unknown tuple tag `{}`", tag),
                        )));
                    }
                }

                LogicalReplicationMessage::Delete(DeleteBody::new(
                    transaction_id,
                    o_id,
                    old_tuple,
                    key_tuple,
                ))
            }
            byte => {
                return Err(ConversionError::Io(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    format!("unknown replication message tag `{}`", byte),
                )));

View on GitHub (pinned to e474e8803c)

Solutions

  1. Inspect the raw frame: a pgoutput DELETE must be 'D', relation OID (u32 BE), then 'K' or 'O', followed by tuple data — fix whatever produced a different byte.
  2. Recreate the replication slot and restart streaming from a clean LSN if corruption is suspected.
  3. Confirm the replica identity setup is understood: 'K' appears with DEFAULT/INDEX identity, 'O' with FULL — never 'N'; update test payloads accordingly.
  4. Ensure no proxy or re-framing layer is mangling the protocol stream.
  5. If a legitimately new pgoutput marker appears (protocol bump), extend the tag match in replication_message.rs after verifying against the PostgreSQL docs.

Example fix

// malformed DELETE payload
let mut buf = [b'D', 0, 0, 0, 7, b'N', /* tuple data */]; // before: 'N' invalid on DELETE
let mut buf = [b'D', 0, 0, 0, 7, b'K', /* key tuple data */]; // after: 'K' = key tuple
Defensive patterns

Strategy: try-catch

Validate before calling

// DELETE frames carry only 'K' or 'O' tuple tags
fn delete_tag_ok(payload: &[u8]) -> bool {
    payload.len() >= 6 && payload[0] == b'D' && matches!(payload[5], b'K' | b'O')
}

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::Delete(d)) => handle_delete(d),
    Ok(msg) => handle(msg),
    Err(e) if e.to_string().contains("unknown tuple tag") => {
        log::warn!("malformed DELETE frame, skipping: {e}");
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling LogicalReplicationMessage::parse on an XLogData payload whose first byte is DELETE_BYTE ('D') and whose tuple tag byte after the relation OID is neither 'K' nor 'O' (e.g. 'N', 0x00, shifted data).

Common situations: Byte misalignment from an earlier truncated/corrupt frame in the same buffer; decoding a delete from a non-pgoutput or modified output plugin; hand-crafted test payloads using 'N' (new tuple is never valid on DELETE); intermediaries that fragment or rewrite replication frames.

Related errors


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