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

  1. Inspect errors.Join output for both the primary error and the close error; fix the primary cause first
  2. Check network stability to the storage backend
  3. If using a custom ReplicaClient, make Close idempotent and ensure it does not fail on already-drained iterators
  4. 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

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


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/31fae8988acf0743. Report an issue: GitHub.