benbjohnson/litestream · warning
close level %d ltx iterator: %w
Error message
close level %d ltx iterator: %w
What it means
When leaving a level's iteration, fillFollowGap closes the LTX file iterator; if itr.Close() fails, the close error is wrapped with the level number. It can be joined with the original return error via errors.Join. Iterator close errors on most replica clients surface flush/HTTP-finalization failures of the underlying listing request.
Source
Thrown at replica.go:1014
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)
if retErr != nil {
return currentTXID, errors.Join(retErr, closeErr)
}
return currentTXID, closeErr
}
return currentTXID, retErr
}
for itr.Next() {
info := itr.Item()
// Skip if there's a gap at this level too.
if info.MinTXID > currentTXID+1 {
break
}
// Skip if already covered.
if info.MaxTXID <= currentTXID {View on GitHub (pinned to 4ed7a308f6)
Solutions
- Inspect errors.Join output for both the primary error and the close error; fix the primary cause first
- Check network stability to the storage backend
- If using a custom ReplicaClient, make Close idempotent and ensure it does not fail on already-drained iterators
- Retry the sync cycle; this is usually transient
Example fix
// before: custom client Close that errors when iterator exhausted
func (c *Client) Close() error { return c.resp.Body.Close() } // resp may be nil
// after
func (c *Client) Close() error {
if c.resp == nil || c.resp.Body == nil { return nil }
return c.resp.Body.Close()
} Defensive patterns
Strategy: try-catch
Validate before calling
// if implementing a custom client, smoke-test iterator lifecycle
itr, err := client.LTXFiles(ctx, 0, 0, false)
if err != nil { return err }
for itr.Next() { _ = itr.Item() }
if err := itr.Err(); err != nil { return err }
if err := itr.Close(); err != nil { return fmt.Errorf("iterator close not idempotent: %w", err) }
if err := itr.Close(); err != nil { return fmt.Errorf("double-close failed: %w", err) } Type guard
type safeIterator interface {
ltx.FileIterator
}
func closeQuietly(itr ltx.FileIterator) error {
if itr == nil { return nil }
return itr.Close()
} Try / catch
txid, err := r.fillFollowGap(ctx, f, afterTXID, gapMinTXID, pageSize)
if err != nil {
for _, e := range unpackJoined(err) { // errors.Join may carry close error too
log.Printf("gap-fill component error: %v", e)
}
// treat close-level errors as transient; retry once
return retryOnce(func() error { _, err = r.fillFollowGap(ctx, f, afterTXID, gapMinTXID, pageSize); return err })
} Prevention
- Implement idempotent Close in custom ReplicaClient iterators
- Always inspect errors.Join components to find the root cause
- Keep network sessions alive; avoid aggressive idle timeouts mid-listing
- Retry replication cycles on transient close/list errors
When it happens
Trigger: itr.Close() returns non-nil after iterating level-1..3 listings: the client's finalization call (e.g., closing an HTTP page-stream or file handle) fails; returned directly when retErr is nil, or joined when an apply error is already being returned.
Common situations: Network drop exactly while closing a paginated S3 list session, storage client implementations whose Close finalizes an in-flight request, custom replica_client.go implementations with faulty Close.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
- close l1 iterator: %w
- replica sync: %w
- fetch ltx files: %w
- cannot determine L%d max ltx file for %q: %w
- write ltx file: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/31fae8988acf0743.
Report an issue: GitHub.