benbjohnson/litestream · error
read header: %w
Error message
read header: %w
What it means
NewWALReaderWithOffset wraps any failure from readHeader() with 'read header: %w'. readHeader reads the 32-byte WAL header and validates magic, checksum, version, page size and salt fields. Most commonly the wrapped error is io.EOF, which readHeader returns when the header checksum does not match — typically because the WAL header was only partially written during a crashed checkpoint.
Source
Thrown at wal_reader.go:55
return nil, err
}
return r, nil
}
// NewWALReaderWithOffset returns a new instance of WALReader at a given offset.
// Salt must match or else no frames will be returned. Checksum calculated from
// from previous page.
func NewWALReaderWithOffset(ctx context.Context, rd io.ReaderAt, offset int64, salt1, salt2 uint32, logger *slog.Logger) (*WALReader, error) {
// Ensure we are not starting on the first page since we need to read the previous.
if offset <= WALHeaderSize {
return nil, fmt.Errorf("offset (%d) must be greater than the wal header size (%d)", offset, WALHeaderSize)
}
r := &WALReader{r: rd, logger: logger}
// Read header to determine page size & byte order.
if err := r.readHeader(); err != nil {
return nil, fmt.Errorf("read header: %w", err)
}
// Load in salt in case the beginning of the file has been overwritten.
r.salt1, r.salt2 = salt1, salt2
// Ensure offset is positioned on a frame start.
frameSize := int64(r.pageSize + WALFrameHeaderSize)
if (offset-WALHeaderSize)%frameSize != 0 {
return nil, fmt.Errorf("unaligned wal offset %d for page size %d", offset, r.pageSize)
}
r.frameN = int((offset - WALHeaderSize) / frameSize)
// Read previous page to load checksum. Context errors are returned as-is
// so callers don't mistake a cancellation for a frame mismatch.
r.frameN--
if _, _, err := r.readFrame(ctx, make([]byte, r.pageSize), false); err != nil {
if ctx.Err() != nil {
return nil, context.Cause(ctx)View on GitHub (pinned to 4ed7a308f6)
Solutions
- Verify the -wal file exists and is at least 32 bytes (ls -l path.wal); an empty/truncated WAL cannot be read
- Treat io.EOF wrapped by this error as 'no valid WAL yet' and skip/wait — SQLite will rewrite the header on the next write
- Confirm only one process is writing the database (a competing checkpoint can truncate the header mid-read)
- If the file is consistently unreadable, restore the database from the latest replica backup and let Litestream re-replicate
Example fix
// before: crashing on truncated WAL
r, err := NewWALReaderWithOffset(ctx, f, offset, logger)
if err != nil { return err }
// after: tolerate not-yet-valid WAL headers
r, err := NewWALReaderWithOffset(ctx, f, offset, logger)
if errors.Is(err, io.EOF) { return nil // WAL header not written yet; retry later }
if err != nil { return err } Defensive patterns
Strategy: validation
Validate before calling
fi, err := os.Stat(walPath)
if err != nil { return err }
if fi.Size() < 32 {
return fmt.Errorf("wal %s too small (%d bytes); not yet written or truncated", walPath, fi.Size())
} Type guard
func hasValidWALSize(fi os.FileInfo) bool { return fi != nil && fi.Size() >= 32 } Try / catch
r, err := NewWALReaderWithOffset(ctx, f, off, logger)
if errors.Is(err, io.EOF) {
// partial/absent WAL header — retry later
return nil
} else if err != nil {
return fmt.Errorf("open wal: %w", err)
} Prevention
- Check WAL file size >= 32 bytes before opening
- Never read the -wal file while a checkpoint is in progress unless the reader tolerates io.EOF
- Monitor for zero-length WAL files after crashes and treat them as benign
- Run a single Litestream process per database to avoid competing checkpoints
When it happens
Trigger: Calling NewWALReaderWithOffset (directly or via sync) on a WAL file that is shorter than 32 bytes, is empty, or whose 32-byte header fails the checksum verification (returning the wrapped io.EOF).
Common situations: A Litestream process or SQLite checkpoint crashed mid-write leaving a zero-length or truncated -wal file; the WAL file was recreated/truncated by another tool; reading a WAL snapshot copied before the header was flushed.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- set synchronous: %w
- checkpoint: %w
- checkpoint failed: %w
- sync database %s: %w
- enable wal failed, mode=%q
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/63210e4094f842a0.
Report an issue: GitHub.