{"record":{"id":"8c262ec649e24927","repo":"windmill-labs/windmill","slug":"unknown-tuple-byte","errorCode":null,"errorMessage":"unknown tuple byte `{}`","messagePattern":"unknown tuple byte `(.+?)`","errorType":"exception","errorClass":"ConversionError::Io (InvalidInput)","httpStatus":null,"severity":"error","filePath":"backend/windmill-trigger-postgres/src/replication_message.rs","lineNumber":413,"sourceCode":"                    TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,\n                    TUPLE_OLD_BYTE | TUPLE_KEY_BYTE => {\n                        if byte == TUPLE_OLD_BYTE {\n                            old_tuple = Some(TupleData::parse(&mut buf)?);\n                        } else {\n                            key_tuple = Some(TupleData::parse(&mut buf)?);\n                        }\n                        match buf.read_u8()? {\n                            TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,\n                            byte => {\n                                return Err(ConversionError::Io(io::Error::new(\n                                    io::ErrorKind::InvalidInput,\n                                    format!(\"unexpected tuple byte `{}`\", byte),\n                                )));\n                            }\n                        }\n                    }\n                    byte => {\n                        return Err(ConversionError::Io(io::Error::new(\n                            io::ErrorKind::InvalidInput,\n                            format!(\"unknown tuple byte `{}`\", byte),\n                        )));\n                    }\n                };\n\n                LogicalReplicationMessage::Update(UpdateBody::new(\n                    transaction_id,\n                    o_id,\n                    old_tuple,\n                    key_tuple,\n                    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,","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-trigger-postgres/src/replication_message.rs#L395-L431","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// hand-built UPDATE payload\nlet mut buf = [b'U', 0, 0, 0, 42, b'Q', /* tuple data */]; // before: 'Q' is invalid\nlet mut buf = [b'U', 0, 0, 0, 42, b'K', /* key tuple */ b'N', /* new tuple */]; // after","handlingStrategy":"try-catch","validationCode":"// check UPDATE frame's tuple markers before parsing\nfn update_marker_ok(payload: &[u8], replica_identity_full: bool) -> bool {\n    payload.len() >= 6\n        && payload[0] == b'U'\n        && matches!(payload[5], b'N' | b'K')\n        && (!replica_identity_full || payload[5] != b'N' || true) // 'O' allowed for FULL\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::Update(u)) => handle_update(u),\n    Ok(msg) => handle(msg),\n    Err(e) if e.to_string().contains(\"unknown tuple byte\") => {\n        log::warn!(\"corrupt UPDATE frame, skipping: {e}\");\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["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"],"tags":["postgres","replication","update-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"}