benbjohnson/litestream · error

open ltx file

Error message

open ltx file: %w

What it means

FetchLTXHeader opens a specific LTX file on the replica (client.OpenLTXFile, restricted to the header bytes) and this error wraps the open failure. Per the RAISED-IN note, it surfaces through Init and related flows (restoreIfNotExists, LTXFiles, WriteLTXFile, OpenLTXFile, DeleteLTXFiles, DeleteAll), meaning an LTX file recorded in the listing could not actually be opened for reading.

Solutions

  1. Re-list the replica (litestream ltx / ValidateLevel) to get current file info — the referenced file may have been removed by retention or compaction
  2. Verify storage credentials allow object reads (e.g. s3:GetObject) and the bucket/endpoint config is correct
  3. If local state is inconsistent, run 'litestream reset' for the database or perform a fresh 'litestream restore'
  4. Check for concurrent processes deleting from the same replica (single replica per database rule)

Example fix

// before: opening a FileInfo captured long ago
rc, err := client.OpenLTXFile(ctx, staleInfo.Level, staleInfo.MinTXID, staleInfo.MaxTXID, 0, ltx.HeaderSize)
// after: re-fetch fresh file info right before opening
info, err := findFile(ctx, client, level, txid) // fresh listing
rc, err := client.OpenLTXFile(ctx, info.Level, info.MinTXID, info.MaxTXID, 0, ltx.HeaderSize)
Defensive patterns

Strategy: retry

Validate before calling

// Re-list to confirm the file still exists before opening
itr, err := client.LTXFiles(ctx, info.Level, info.MinTXID, false)
if err == nil {
    var found bool
    for itr.Next() { if itr.Item().MaxTXID == info.MaxTXID { found = true; break } }
    if !found { return fmt.Errorf("LTX file %s no longer present", info.MaxTXID) }
}

Type guard

func fileStillListed(ctx context.Context, c ReplicaClient, info *ltx.FileInfo) bool {
    itr, err := c.LTXFiles(ctx, info.Level, info.MinTXID, false)
    if err != nil { return false }
    defer itr.Close()
    for itr.Next() {
        it := itr.Item()
        if it.MinTXID == info.MinTXID && it.MaxTXID == info.MaxTXID { return true }
    }
    return false
}

Try / catch

hdr, err := FetchLTXHeader(ctx, client, info)
if err != nil {
    if errors.Is(err, os.ErrNotExist) || isNotFoundErr(err) {
        // re-list and pick a fresher restore candidate
    }
    return err
}

Prevention

When it happens

Trigger: OpenLTXFile called with (level, MinTXID, MaxTXID) that does not resolve to a stored object: the file was deleted by retention/compaction between listing and open, the object name is wrong, storage credentials lack read access, or the object is missing in the bucket/container.

Common situations: A retention job or manual cleanup deleted LTX files while a restore/validation was in progress; replica URL points to a bucket where compaction removed the referenced generation; misconfigured endpoint causing 404-style open failures; IAM policy missing s3:GetObject.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at replica_client.go:104

// DefaultEstimatedPageIndexSize is size that is first fetched when fetching the page index.
// If the fetch was smaller than the actual page index, another call is made to fetch the rest.
const DefaultEstimatedPageIndexSize = 32 * 1024 // 32KB

func FetchPageIndex(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (map[uint32]ltx.PageIndexElem, error) {
	rc, err := fetchPageIndexData(ctx, client, info)
	if err != nil {
		return nil, err
	}
	defer rc.Close()

	return ltx.DecodePageIndex(bufio.NewReader(rc), info.Level, info.MinTXID, info.MaxTXID)
}

// FetchLTXHeader reads & returns the LTX header for the given file info.
func FetchLTXHeader(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (ltx.Header, error) {
	rc, err := client.OpenLTXFile(ctx, info.Level, info.MinTXID, info.MaxTXID, 0, ltx.HeaderSize)
	if err != nil {
		return ltx.Header{}, fmt.Errorf("open ltx file: %w", err)
	}
	defer rc.Close()
	hdr, _, err := ltx.PeekHeader(rc)
	if err != nil {
		return ltx.Header{}, fmt.Errorf("peek header: %w", err)
	}
	return hdr, nil
}

// fetchPageIndexData fetches a chunk of the end of the file to get the page index.
// If the fetch was smaller than the actual page index, another call is made to fetch the rest.
func fetchPageIndexData(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (io.ReadCloser, error) {
	// Fetch the end of the file to get the page index.
	offset := info.Size - DefaultEstimatedPageIndexSize
	if offset < 0 {
		offset = 0
	}

View on GitHub (pinned to 4ed7a308f6)