vxcontrol/pentagi · warning

listing container directory '%s': %w

Error message

listing container directory '%s': %w

What it means

After stat'ing every entry, ListContainerDir checks ctx.Err() and fails the whole listing if the context was cancelled, treating cancellation as a directory-level fault (the client is gone) rather than returning a partial listing. The wrapped cause is context.Canceled or context.DeadlineExceeded.

Source

Thrown at backend/pkg/docker/client.go:1025

	if err != nil {
		return ContainerDirListing{}, fmt.Errorf("failed to inspect list exec for '%s': %w", dirPath, err)
	}
	if inspect.ExitCode != 0 {
		return ContainerDirListing{}, fmt.Errorf("list command failed for '%s' with exit code %d: %s", dirPath, inspect.ExitCode, string(output))
	}

	entryPaths, truncated := parseFindEntries(output)

	stats, failures := statContainerEntries(ctx, entryPaths, containerListWorkers, func(ctx context.Context, entryPath string) (container.PathStat, error) {
		return dc.ContainerStatPath(ctx, containerID, entryPath)
	})

	// A cancelled context is a directory-level fault (the client is gone), not a
	// partial listing, so surface it as an error. Entries that individually failed
	// to stat are carried in Failures — the find exec already proved the container
	// alive, so a live directory degrades rather than 500s even if every entry failed.
	if err := ctx.Err(); err != nil {
		return ContainerDirListing{}, fmt.Errorf("listing container directory '%s': %w", dirPath, err)
	}

	listing := ContainerDirListing{Files: stats, Truncated: truncated}
	for _, f := range failures {
		listing.Failures = append(listing.Failures, ContainerEntryError{
			Name: path.Base(f.name),
			Path: f.name,
			Err:  f.err,
		})
	}

	return listing, nil
}

// parseFindEntries splits `find -print0` output (NUL-delimited absolute paths),
// dropping empties, and caps the result at maxListEntries — returning truncated=true
// so the caller reports the partiality instead of failing or silently dropping.
func parseFindEntries(output []byte) (entries []string, truncated bool) {

View on GitHub (pinned to ea665308ba)

Solutions

  1. Increase the context deadline passed to ListContainerDir for large directories
  2. List a narrower directory to reduce per-entry stat work
  3. Ensure the caller does not cancel the context before results are consumed
  4. Retry with a fresh, longer-lived context if the cancellation was transient

Example fix

// before: short timeout expires mid-listing
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
// after: size the deadline to the directory
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
listing, err := dc.ListContainerDir(ctx, id, dir)
Defensive patterns

Strategy: try-catch

Try / catch

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
listing, err := client.ListContainerDir(ctx, containerID, dir)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
        return retryListingWithLargerDeadline(containerID, dir)
    }
    return err
}

Prevention

When it happens

Trigger: The caller's context is cancelled or its deadline expires while ListContainerDir is running (per-entry ContainerStatPath calls against a large directory), or the HTTP request that triggered the listing is aborted by the client.

Common situations: Listing huge directories against a slow/remote daemon exceeding deadlines; aborted HTTP requests; service shutdown draining in-flight work.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/845e4320404b6a6b. Report an issue: GitHub.