benbjohnson/litestream · error

webdav: cannot delete ltx file %q: %w

Error message

webdav: cannot delete ltx file %q: %w

What it means

This error wraps an underlying failure returned by the WebDAV server's Remove() call when Litestream tries to delete an LTX (replication) file. It is only raised when the error is neither os.IsNotExist nor a gowebdav IsErrNotFound, i.e. a real failure rather than the file already being gone. Litestream surfaces the offending filename and the wrapped cause so the replication cleanup can be diagnosed.

Source

Thrown at webdav/replica_client.go:365

		}
		return nil, fmt.Errorf("webdav: cannot read file %q: %w", filename, err)
	}
	return rc, nil
}

func (c *ReplicaClient) DeleteLTXFiles(ctx context.Context, a []*ltx.FileInfo) error {
	client, err := c.init(ctx)
	if err != nil {
		return err
	}

	for _, info := range a {
		filename := litestream.LTXFilePath(c.Path, info.Level, info.MinTXID, info.MaxTXID)

		c.logger.Debug("deleting ltx file", "level", info.Level, "minTXID", info.MinTXID, "maxTXID", info.MaxTXID, "path", filename)

		if err := client.Remove(filename); err != nil && !os.IsNotExist(err) && !gowebdav.IsErrNotFound(err) {
			return fmt.Errorf("webdav: cannot delete ltx file %q: %w", filename, err)
		}
		internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "DELETE").Inc()
	}

	return nil
}

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Check WebDAV credentials and that the account has DELETE permission on the replica path
  2. Read the wrapped cause (%w) in logs to identify the HTTP status from gowebdav and fix the server-side condition
  3. Verify the litestream.yml webdav url/path is correct and reachable (test with curl -X DELETE)
  4. Retry after transient network/server errors; Litestream will retry the sync loop and re-attempt deletion
  5. If files were already removed out-of-band, no action needed: not-found errors are tolerated

Example fix

// before: read-only WebDAV credentials cause 403
collector: litestream.yml webdav:
  url: https://dav.example.com/litestream
  username: reader
  password: ****
// after: use credentials with write access
  url: https://dav.example.com/litestream
  username: writer
  password: ****
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: check WebDAV reachability and permissions before deleting
client := gowebdav.NewClient(url)
client.SetBasicAuth(user, pass)
if err := client.Connect(); err != nil {
    return fmt.Errorf("webdav unreachable: %w", err)
}
if _, err := client.ReadDir(basePath); err != nil {
    return fmt.Errorf("no read/write access to %s: %w", basePath, err)
}

Type guard

func isNotFoundErr(err error) bool {
    return os.IsNotExist(err) || gowebdav.IsErrNotFound(err)
}
// treat only non-not-found errors as fatal, like litestream does

Try / catch

if err := replicaClient.DeleteLTXFiles(ctx, infos); err != nil {
    var davErr *gowebdav.StatusError
    if errors.As(err, &davErr) {
        log.Printf("webdav delete failed status=%d: %v", davErr.Status, davErr)
    } else {
        log.Printf("webdav delete failed: %v", err)
    }
    // transient: let the next sync retry
    return err
}

Prevention

When it happens

Trigger: Calling ReplicaClient.DeleteLTXFiles on a WebDAV replica when the server returns 403 Forbidden (read-only share/credentials), 401 Unauthorized (bad password), 500 (server-side error), a connection failure/time-out mid-delete, or a path escaping issues (bad c.Path base URL).

Common situations: WebDAV account lacks write permission on the replica directory; server returned 401 due to rotated credentials not updated in config; reverse proxy or gateway returned 5xx; transient network drop during retention cleanup; misconfigured path in litestream.yml.

Related errors


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