benbjohnson/litestream · error
invalid wal header magic: %x
Error message
invalid wal header magic: %x
What it means
readHeader validates that the first 4 bytes of the WAL file are SQLite's WAL magic numbers 0x377f0682 (little-endian) or 0x377f0683 (big-endian). Any other value means the file being read is not a valid SQLite WAL, or its header is corrupt/truncated.
Source
Thrown at wal_reader.go:110
// readHeader reads the WAL header into the reader. Returns io.EOF if WAL is invalid.
func (r *WALReader) readHeader() error {
// If we have a partial WAL, then mark WAL as done.
hdr := make([]byte, WALHeaderSize)
if n, err := r.r.ReadAt(hdr, 0); n < len(hdr) {
return io.EOF
} else if err != nil {
return err
}
// Determine byte order of checksums.
switch magic := binary.BigEndian.Uint32(hdr[0:]); magic {
case 0x377f0682:
r.bo = binary.LittleEndian
case 0x377f0683:
r.bo = binary.BigEndian
default:
return fmt.Errorf("invalid wal header magic: %x", magic)
}
// If the header checksum doesn't match then we may have failed with
// a partial WAL header write during checkpointing.
chksum1 := binary.BigEndian.Uint32(hdr[24:])
chksum2 := binary.BigEndian.Uint32(hdr[28:])
if v0, v1 := WALChecksum(r.bo, 0, 0, hdr[:24]); v0 != chksum1 || v1 != chksum2 {
return io.EOF
}
// Verify version is correct.
if version := binary.BigEndian.Uint32(hdr[4:]); version != 3007000 {
return fmt.Errorf("unsupported wal version: %d", version)
}
r.pageSize = binary.BigEndian.Uint32(hdr[8:])
r.seq = binary.BigEndian.Uint32(hdr[12:])
r.salt1 = binary.BigEndian.Uint32(hdr[16:])View on GitHub (pinned to 4ed7a308f6)
Solutions
- Confirm the path passed to the reader is the '-wal' file, not the main database file
- Check the first 4 bytes of the file (xxd -l 4 file) — they must be 37 7f 06 82 or 37 7f 06 83
- If corruption is suspected, restore from the latest replica and let SQLite recreate the WAL
- Ensure no other process truncated or rewrote the file while it was being read
Defensive patterns
Strategy: validation
Validate before calling
hdr := make([]byte, 4)
f, _ := os.Open(walPath)
io.ReadFull(f, hdr)
magic := binary.BigEndian.Uint32(hdr)
if magic != 0x377f0682 && magic != 0x377f0683 {
return fmt.Errorf("%s is not a SQLite WAL file (magic %x)", walPath, magic)
} Type guard
func isWALMagic(b []byte) bool {
m := binary.BigEndian.Uint32(b[:4])
return m == 0x377f0682 || m == 0x377f0683
} Try / catch
r, err := NewWALReader(ctx, f, logger)
if err != nil && strings.Contains(err.Error(), "invalid wal header magic") {
return fmt.Errorf("%s is not a WAL file — check configured path: %w", f.Name(), err)
} Prevention
- Confirm the configured path points to the '-wal' file, not the database itself
- Verify magic bytes 37 7f 06 82/83 before parsing third-party files
- Avoid reading the WAL during concurrent checkpoint/truncation
- Restore from replica when the header is corrupt instead of forcing a read
When it happens
Trigger: Opening a file as a WAL that is actually something else (a database file, an LTX file, a log, an empty file with garbage); reading a WAL whose first bytes were overwritten by disk corruption or an interrupted header write.
Common situations: Misconfigured WAL path in config pointing at the .db file instead of the -wal file; copying/mounting volumes while SQLite was mid-checkpoint; testing the reader against synthetic or placeholder files.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/1c6b6539204f93b2.
Report an issue: GitHub.