{"record":{"id":"7515536eb1797f52","repo":"windmill-labs/windmill","slug":"unknown-tuple-tag","errorCode":null,"errorMessage":"unknown tuple tag `{}`","messagePattern":"unknown tuple tag `(.+?)`","errorType":"exception","errorClass":"ConversionError::Io (InvalidInput)","httpStatus":null,"severity":"error","filePath":"backend/windmill-trigger-postgres/src/replication_message.rs","lineNumber":443,"sourceCode":"                    new_tuple,\n                ))\n            }\n            DELETE_BYTE => {\n                let transaction_id = match logical_replication_settings.streaming {\n                    true => Some(buf.read_i32::<BigEndian>()?),\n                    false => None,\n                };\n                let o_id = buf.read_u32::<BigEndian>()?;\n                let tag = buf.read_u8()?;\n\n                let mut key_tuple = None;\n                let mut old_tuple = None;\n\n                match tag {\n                    TUPLE_OLD_BYTE => old_tuple = Some(TupleData::parse(&mut buf)?),\n                    TUPLE_KEY_BYTE => key_tuple = Some(TupleData::parse(&mut buf)?),\n                    tag => {\n                        return Err(ConversionError::Io(io::Error::new(\n                            io::ErrorKind::InvalidInput,\n                            format!(\"unknown tuple tag `{}`\", tag),\n                        )));\n                    }\n                }\n\n                LogicalReplicationMessage::Delete(DeleteBody::new(\n                    transaction_id,\n                    o_id,\n                    old_tuple,\n                    key_tuple,\n                ))\n            }\n            byte => {\n                return Err(ConversionError::Io(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    format!(\"unknown replication message tag `{}`\", byte),\n                )));","sourceCodeStart":425,"sourceCodeEnd":461,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-trigger-postgres/src/replication_message.rs#L425-L461","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","Recreate the replication slot and restart streaming from a clean LSN if corruption is suspected.","Confirm the replica identity setup is understood: 'K' appears with DEFAULT/INDEX identity, 'O' with FULL — never 'N'; update test payloads accordingly.","Ensure no proxy or re-framing layer is mangling the protocol stream.","If a legitimately new pgoutput marker appears (protocol bump), extend the tag match in replication_message.rs after verifying against the PostgreSQL docs."],"exampleFix":"// malformed DELETE payload\nlet mut buf = [b'D', 0, 0, 0, 7, b'N', /* tuple data */]; // before: 'N' invalid on DELETE\nlet mut buf = [b'D', 0, 0, 0, 7, b'K', /* key tuple data */]; // after: 'K' = key tuple","handlingStrategy":"try-catch","validationCode":"// DELETE frames carry only 'K' or 'O' tuple tags\nfn delete_tag_ok(payload: &[u8]) -> bool {\n    payload.len() >= 6 && payload[0] == b'D' && matches!(payload[5], b'K' | b'O')\n}","typeGuard":"fn is_conversion_error(err: &std::io::Error) -> bool {\n    err.kind() == std::io::ErrorKind::InvalidInput\n}","tryCatchPattern":"match LogicalReplicationMessage::parse(buf, settings) {\n    Ok(LogicalReplicationMessage::Delete(d)) => handle_delete(d),\n    Ok(msg) => handle(msg),\n    Err(e) if e.to_string().contains(\"unknown tuple tag\") => {\n        log::warn!(\"malformed DELETE frame, skipping: {e}\");\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Fix test payloads: DELETE frames never carry 'N'; use 'K' (key) or 'O' (old, replica identity FULL)","Ensure every replicated table has an intentional replica identity so delete frames are well-formed","Monitor for repeated decode failures and recreate the slot — repeated errors usually mean stream corruption, not protocol novelty","Only parse payloads captured from the pgoutput logical replication connection, never from other COPY channels"],"tags":["postgres","replication","delete-message","protocol-decoding"],"backgroundTag":"replication-protocol-parse-error","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}