{"record":{"id":"63210e4094f842a0","repo":"benbjohnson/litestream","slug":"read-header-w","errorCode":null,"errorMessage":"read header: %w","messagePattern":"read header: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"wal_reader.go","lineNumber":55,"sourceCode":"\t\treturn nil, err\n\t}\n\treturn r, nil\n}\n\n// NewWALReaderWithOffset returns a new instance of WALReader at a given offset.\n// Salt must match or else no frames will be returned. Checksum calculated from\n// from previous page.\nfunc NewWALReaderWithOffset(ctx context.Context, rd io.ReaderAt, offset int64, salt1, salt2 uint32, logger *slog.Logger) (*WALReader, error) {\n\t// Ensure we are not starting on the first page since we need to read the previous.\n\tif offset <= WALHeaderSize {\n\t\treturn nil, fmt.Errorf(\"offset (%d) must be greater than the wal header size (%d)\", offset, WALHeaderSize)\n\t}\n\n\tr := &WALReader{r: rd, logger: logger}\n\n\t// Read header to determine page size & byte order.\n\tif err := r.readHeader(); err != nil {\n\t\treturn nil, fmt.Errorf(\"read header: %w\", err)\n\t}\n\n\t// Load in salt in case the beginning of the file has been overwritten.\n\tr.salt1, r.salt2 = salt1, salt2\n\n\t// Ensure offset is positioned on a frame start.\n\tframeSize := int64(r.pageSize + WALFrameHeaderSize)\n\tif (offset-WALHeaderSize)%frameSize != 0 {\n\t\treturn nil, fmt.Errorf(\"unaligned wal offset %d for page size %d\", offset, r.pageSize)\n\t}\n\tr.frameN = int((offset - WALHeaderSize) / frameSize)\n\n\t// Read previous page to load checksum. Context errors are returned as-is\n\t// so callers don't mistake a cancellation for a frame mismatch.\n\tr.frameN--\n\tif _, _, err := r.readFrame(ctx, make([]byte, r.pageSize), false); err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, context.Cause(ctx)","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/wal_reader.go#L37-L73","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: crashing on truncated WAL\nr, err := NewWALReaderWithOffset(ctx, f, offset, logger)\nif err != nil { return err }\n// after: tolerate not-yet-valid WAL headers\nr, err := NewWALReaderWithOffset(ctx, f, offset, logger)\nif errors.Is(err, io.EOF) { return nil // WAL header not written yet; retry later }\nif err != nil { return err }","handlingStrategy":"validation","validationCode":"fi, err := os.Stat(walPath)\nif err != nil { return err }\nif fi.Size() < 32 {\n    return fmt.Errorf(\"wal %s too small (%d bytes); not yet written or truncated\", walPath, fi.Size())\n}","typeGuard":"func hasValidWALSize(fi os.FileInfo) bool { return fi != nil && fi.Size() >= 32 }","tryCatchPattern":"r, err := NewWALReaderWithOffset(ctx, f, off, logger)\nif errors.Is(err, io.EOF) {\n    // partial/absent WAL header — retry later\n    return nil\n} else if err != nil {\n    return fmt.Errorf(\"open wal: %w\", err)\n}","preventionTips":["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"],"tags":["sqlite","wal","file-io","checksum"],"backgroundTag":"checksum-mismatch","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}