benbjohnson/litestream · error

webdav: cannot read directory %q: %w

Error message

webdav: cannot read directory %q: %w

What it means

LTXFiles lists the LTX files for a compaction level by reading the directory <path>/<level> on the WebDAV server. Missing directories are treated as an empty file list (no error), but any other ReadDir failure (permissions, server error, timeout) is wrapped with this message.

Source

Thrown at webdav/replica_client.go:148

	internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc()

	return nil
}

func (c *ReplicaClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, _ bool) (_ ltx.FileIterator, err error) {
	client, err := c.init(ctx)
	if err != nil {
		return nil, err
	}

	dir := litestream.LTXLevelDir(c.Path, level)
	files, err := client.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) || gowebdav.IsErrNotFound(err) {
			return ltx.NewFileInfoSliceIterator(nil), nil
		}
		return nil, fmt.Errorf("webdav: cannot read directory %q: %w", dir, err)
	}

	infos := make([]*ltx.FileInfo, 0, len(files))
	for _, fi := range files {
		if fi.IsDir() {
			continue
		}

		minTXID, maxTXID, err := ltx.ParseFilename(path.Base(fi.Name()))
		if err != nil {
			continue
		} else if minTXID < seek {
			continue
		}

		infos = append(infos, &ltx.FileInfo{
			Level:     level,
			MinTXID:   minTXID,

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Verify the directory is listable: curl -u user:pass -X PROPFIND https://host/<path>/0 -H 'Depth: 1'
  2. Check permissions on the LTX level directories for the configured WebDAV user
  3. Look at the wrapped error for the root cause (status code, timeout, TLS) and fix accordingly
  4. Retry on transient errors; note a 404 is already handled gracefully as an empty iterator
  5. If the replica tree is broken, re-seed it with a fresh 'litestream replicate' run
Defensive patterns

Strategy: fallback

Validate before calling

dir := litestream.LTXLevelDir(path, level)
req, _ := http.NewRequest(http.MethodPropfind, baseURL+dir, nil)
req.SetBasicAuth(user, pass)
resp, err := httpClient.Do(req)
if err != nil { return err }
resp.Body.Close()
if resp.StatusCode != 207 && resp.StatusCode != 404 {
    return fmt.Errorf("PROPFIND %s: %s", dir, resp.Status)
}

Try / catch

it, err := rc.LTXFiles(ctx, level)
if err != nil && strings.Contains(err.Error(), "cannot read directory") {
    // fall back to empty listing and retry later, or surface the wrapped cause
    return fmt.Errorf("listing level %d failed: %w", level, err)
}

Prevention

When it happens

Trigger: Listing replicas ('litestream replicas' / restore path / retention scans) when the WebDAV server returns a non-404 error for PROPFIND on the level directory: 403, 500, connection drop, or timeout.

Common situations: WebDAV user lacking read permission on the directory; server outage mid-listing; transient network failure; server-side path permissions changed after the replica was created; directory exists but PROPFIND depth is restricted by the server.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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