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

unknown replication message tag `{}`

Error message

unknown replication message tag `{}`

What it means

The top-level dispatcher inside LogicalReplicationMessage::parse only recognizes the pgoutput logical message tags: 'B' (Begin), 'C' (Commit), 'R' (Relation), 'Y' (Type), 'I' (Insert), 'U' (Update), 'D' (Delete). This error fires when the first byte of the logical message payload is none of those, meaning the XLogData body is not a logical-decoding message the library understands. Decoding aborts with an InvalidInput io::Error wrapped in ConversionError.

Source

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

                    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),
                )));
            }
        };

        Ok(logical_replication_message)
    }
}

#[non_exhaustive]
#[derive(Debug)]
pub enum ReplicationMessage {
    XLogData(XLogDataBody),
    PrimaryKeepAlive(PrimaryKeepAliveBody),
}

impl ReplicationMessage {

View on GitHub (pinned to e474e8803c)

Solutions

  1. Check which pgoutput protocol version the slot was created with; drop and recreate the slot with a protocol version/options the parser supports (no streamed or two-phase transactions).
  2. Disable options that emit unsupported message tags: avoid pg_logical_emit_message usage and REORDER/WRITE in binary mode until the parser handles those tags.
  3. Verify you only feed the XLogData payload (the inner data of 'w' CopyData frames) to LogicalReplicationMessage::parse, not the outer ReplicationMessage bytes.
  4. Look at the reported byte and cross-check it against the PostgreSQL logical replication message docs to identify what message type your server is sending.
  5. If the byte is a valid newer pgoutput tag (e.g. 'M', 'S'), extend the match in replication_message.rs to skip or decode it.
Defensive patterns

Strategy: validation

Validate before calling

// reject unsupported pgoutput options before opening the slot
fn validate_slot_options(protocol_version: i32, streaming: bool, two_phase: bool) -> Result<(), String> {
    if protocol_version > 1 {
        return Err("protocol_version >1 emits streaming tags this parser does not know".into());
    }
    if streaming || two_phase {
        return Err("streamed/two-phase transactions emit 'S'/'E'/'c' tags unsupported by the parser".into());
    }
    Ok(())
}

Type guard

fn is_known_logical_tag(payload: &[u8]) -> bool {
    matches!(payload.first(), Some(b'B' | b'C' | b'R' | b'Y' | b'I' | b'U' | b'D'))
}

Try / catch

match LogicalReplicationMessage::parse(buf, settings) {
    Ok(msg) => handle(msg),
    Err(e) if e.to_string().contains("unknown replication message tag") => {
        let tag = /* extract from message */;
        log::warn!("unsupported pgoutput tag {tag:?}; check slot protocol options");
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling LogicalReplicationMessage::parse with a buffer whose first byte is not one of B/C/R/Y/I/U/D — e.g. passing a raw COPY-both protocol frame, a keepalive, or an unknown logical message tag such as 'M' (logical decoding message) or 'S' (streaming start/stop) that the parser does not implement.

Common situations: Enabling protocol options the parser lacks support for — e.g. protocol_version >= 2/3 with streamed (two-phase) transactions emitting 'S'/'s'/'E'/'c' streaming tags, or pg_logical_emit_message producing 'M' messages; pointing the parser at non-XLogData bytes; truncation shifting the stream into message bodies; PostgreSQL version emitting newer tag types.

Related errors


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