{"record":{"id":"842f0b720e154e53","repo":"neondatabase/neon","slug":"unknown-rmgr-for-heap-decoding","errorCode":null,"errorMessage":"Unknown RMGR {} for Heap decoding","messagePattern":"Unknown RMGR (.+?) for Heap decoding","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/wal_decoder/src/decoder.rs","lineNumber":272,"sourceCode":"                                // the offsets array is omitted if XLOG_HEAP_INIT_PAGE is set\n                                0\n                            } else {\n                                size_of::<u16>() * xlrec.ntuples as usize\n                            };\n                        assert_eq!(offset_array_len, buf.remaining());\n\n                        if (xlrec.flags & pg_constants::XLH_INSERT_ALL_VISIBLE_CLEARED) != 0 {\n                            new_heap_blkno = Some(decoded.blocks[0].blkno);\n                        }\n                    } else if info == pg_constants::XLOG_HEAP2_LOCK_UPDATED {\n                        let xlrec = v14::XlHeapLockUpdated::decode(buf);\n                        if (xlrec.flags & pg_constants::XLH_LOCK_ALL_FROZEN_CLEARED) != 0 {\n                            old_heap_blkno = Some(decoded.blocks[0].blkno);\n                            flags = pg_constants::VISIBILITYMAP_ALL_FROZEN;\n                        }\n                    }\n                } else {\n                    anyhow::bail!(\"Unknown RMGR {} for Heap decoding\", decoded.xl_rmid);\n                }\n            }\n            PgMajorVersion::PG15 => {\n                if decoded.xl_rmid == pg_constants::RM_HEAP_ID {\n                    let info = decoded.xl_info & pg_constants::XLOG_HEAP_OPMASK;\n\n                    if info == pg_constants::XLOG_HEAP_INSERT {\n                        let xlrec = v15::XlHeapInsert::decode(buf);\n                        assert_eq!(0, buf.remaining());\n                        if (xlrec.flags & pg_constants::XLH_INSERT_ALL_VISIBLE_CLEARED) != 0 {\n                            new_heap_blkno = Some(decoded.blocks[0].blkno);\n                        }\n                    } else if info == pg_constants::XLOG_HEAP_DELETE {\n                        let xlrec = v15::XlHeapDelete::decode(buf);\n                        if (xlrec.flags & pg_constants::XLH_DELETE_ALL_VISIBLE_CLEARED) != 0 {\n                            new_heap_blkno = Some(decoded.blocks[0].blkno);\n                        }\n                    } else if info == pg_constants::XLOG_HEAP_UPDATE","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/wal_decoder/src/decoder.rs#L254-L290","documentation":"`decode_heapam_record` in wal_decoder tracks visibility-map bit updates for heap records, per PG major version. Within the PG14 branch it only accepts `RM_HEAP_ID` and `RM_HEAP2_ID`; this bail fires when `decoded.xl_rmid` is anything else. Since callers are expected to route only Heap/Heap2 records here, hitting it means a misrouted record or a pg_version that doesn't match the WAL being decoded.","triggerScenarios":"Decoding a PG14 WAL stream where a record with `xl_rmid` other than RM_HEAP_ID/RM_HEAP2_ID is fed into the heap decoder — new dispatch code forwarding all record types, or `pg_version` mismatched with the actual WAL.","commonSituations":"Extending wal_decoder to new record types and misrouting; an off-by-one version mapping after a PG release; fuzz tests feeding arbitrary rmids into the heap path.","solutions":["Fix the dispatch site: only records with xl_rmid RM_HEAP_ID or RM_HEAP2_ID should reach heap decoding","Verify the pg_version passed to the decoder matches the cluster that produced the WAL","If adding a new RMGR, add its own handler branch instead of routing through decode_heapam_record","Log decoded.xl_rmid and xl_info upstream to identify the offending record"],"exampleFix":"// before: every record is sent to heap decoding\ndecode_heapam_record(buf, decoded, pg_version)?;\n\n// after: only Heap/Heap2 records\nif matches!(decoded.xl_rmid, pg_constants::RM_HEAP_ID | pg_constants::RM_HEAP2_ID) {\n    decode_heapam_record(buf, decoded, pg_version)?;\n}","handlingStrategy":"validation","validationCode":"if !matches!(decoded.xl_rmid, pg_constants::RM_HEAP_ID | pg_constants::RM_HEAP2_ID) {\n    // not a heap record: route to its own handler or skip\n    return Ok(None);\n}","typeGuard":"fn is_heap_record(rmid: u8) -> bool {\n    matches!(rmid, pg_constants::RM_HEAP_ID | pg_constants::RM_HEAP2_ID)\n}","tryCatchPattern":null,"preventionTips":["Keep the pg_version used for decoding tied to the WAL source, not a free parameter","When adding RMGRs, extend the dispatcher match instead of the heap branch","Fuzz the decoder with arbitrary rmids to assert correct routing"],"tags":["rust","neon","wal-decoding","postgres","pg14"],"backgroundTag":"unsupported-wal-record-type","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}