benbjohnson/litestream · error
close decoder
Error message
close decoder: %w
What it means
After writing pages and truncating, applyLTXFile closes the LTX decoder. dec.Close() verifies the LTX file's trailing checksum over everything decoded; an error here means the transaction stream failed integrity verification even though the data decoded fine. Litestream must fail the apply because the source file cannot be trusted.
Solutions
- Treat the remote LTX file as corrupt: remove/quarantine it and let the replica re-upload
- Run `litestream reset <db>` to clear local state and re-sync from a known-good snapshot
- Inspect the storage object (etag/size) against what was uploaded to confirm corruption
- Ensure nothing mutates LTX files after creation, including lifecycle/lambda processing
Example fix
// before: ignoring decoder close error
dec.Close()
return f.Sync()
// after
if err := dec.Close(); err != nil {
return fmt.Errorf("close decoder: %w", err) // checksum verification failed
} Defensive patterns
Strategy: try-catch
Validate before calling
// verify an LTX object end-to-end before applying
func verifyLTX(ctx context.Context, rc io.ReadCloser) error {
dec := ltx.NewDecoder(rc)
if err := dec.DecodeHeader(); err != nil { return err }
buf := make([]byte, dec.Header().PageSize)
for {
var ph ltx.PageHeader
if err := dec.DecodePage(&ph, buf); err == io.EOF { break } else if err != nil { return err }
}
return dec.Close() // checksum verification happens here
} Try / catch
if err := r.applyLTXFile(ctx, f, info, pageSize); err != nil {
if strings.Contains(err.Error(), "close decoder") {
// checksum verification failed: quarantine object, reset state
quarantineObject(info)
exec.Command("litestream", "reset", dbPath).Run()
}
return err
} Prevention
- Never write to or post-process LTX objects after upload
- Compare uploaded object checksums (S3 etag/MD5) against local file checksums
- Disable lifecycle rules that could alter replica objects
- Keep writer and reader litestream versions compatible
When it happens
Trigger: dec.Close() returns non-nil: LTX checksum mismatch at end-of-stream, trailing bytes after the last page, or the reader rc returned fewer/more bytes than the header declared (e.g., storage returned a corrupted or partial object).
Common situations: Bit rot or partial multipart uploads in object storage, a proxy truncating the HTTP body, LTX file modified after creation (LTX files are immutable by contract), version mismatch between writer and reader.
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
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/5d1c5dc22599d861.
Report an issue: GitHub.
Appendix: source
Thrown at replica.go:996
off := int64(phdr.Pgno-1) * int64(pageSize)
if _, err := f.WriteAt(data, off); err != nil {
return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
}
}
if hdr.Commit > 0 {
if err := f.Sync(); err != nil {
return fmt.Errorf("sync before truncate: %w", err)
}
newSize := int64(hdr.Commit) * int64(pageSize)
if err := f.Truncate(newSize); err != nil {
return fmt.Errorf("truncate: %w", err)
}
}
if err := dec.Close(); err != nil {
return fmt.Errorf("close decoder: %w", err)
}
return f.Sync()
}
// fillFollowGap attempts to bridge a gap in level 0 files by searching
// higher compaction levels for a file that covers the missing TXID range.
func (r *Replica) fillFollowGap(ctx context.Context, f *os.File, afterTXID ltx.TXID, gapMinTXID ltx.TXID, pageSize uint32) (ltx.TXID, error) {
currentTXID := afterTXID
for level := 1; level < SnapshotLevel; level++ {
itr, err := r.Client.LTXFiles(ctx, level, 0, false)
if err != nil {
return currentTXID, fmt.Errorf("list level %d ltx files: %w", level, err)
}
closeLevel := func(retErr error) (ltx.TXID, error) {
if closeErr := itr.Close(); closeErr != nil {
closeErr = fmt.Errorf("close level %d ltx iterator: %w", level, closeErr)View on GitHub (pinned to 4ed7a308f6)