vxcontrol/pentagi · error
failed to stat blob %s: %w
Error message
failed to stat blob %s: %w
What it means
ZipResources stats every blob with os.Lstat before writing any bytes to the ZIP writer, because the HTTP caller may have already sent a 200 status by the time the first byte is written — a missing blob mid-stream would yield a truncated archive under a success code. This error reports which blob path could not be stat'ed and why.
Source
Thrown at backend/pkg/resources/resources.go:331
// Already exists — remove tmp and consider success.
os.Remove(tmpPath)
return nil
}
if err := os.Rename(tmpPath, dest); err != nil {
return fmt.Errorf("failed to commit blob %s: %w", hash, err)
}
return nil
}
// ZipResources writes a ZIP archive to w containing all entries in files.
// Each ZipEntry maps a .blob file on disk to a path inside the archive.
func ZipResources(w io.Writer, entries []ZipEntry) (err error) {
// The streaming HTTP caller commits its 200 status on the first byte written,
// so a missing blob must be caught before then, or the client gets a truncated
// archive under 200. Stat all blobs up front; don't fold into the write loop.
for _, e := range entries {
if _, statErr := os.Lstat(e.BlobPath); statErr != nil {
return fmt.Errorf("failed to stat blob %s: %w", e.BlobPath, statErr)
}
}
zw := zip.NewWriter(w)
defer func() {
if cerr := zw.Close(); err == nil {
err = cerr
}
}()
for _, e := range entries {
info, err := os.Lstat(e.BlobPath)
if err != nil {
return fmt.Errorf("failed to stat blob %s: %w", e.BlobPath, err)
}
if !info.Mode().IsRegular() {
continue
}View on GitHub (pinned to ea665308ba)
Solutions
- Verify the blob file exists at the exact BlobPath (ls the path from the error) and fix the path construction
- Check hex-case consistency: md5 hashes must be produced and looked up with the same encoding (hex.EncodeToString is lowercase)
- Check for GC/cleanup jobs removing .blob files before download and coordinate retention windows
- Verify the blob storage root configuration matches between upload and download paths
Example fix
// before blobPath := filepath.Join(root, strings.ToUpper(hash)) // wrong case // after blobPath := filepath.Join(root, hex.EncodeToString(sum)) // lowercase, matches CommitBlob
Defensive patterns
Strategy: validation
Validate before calling
func validateZipEntries(entries []resources.ZipEntry) error {
for _, e := range entries {
if _, err := os.Lstat(e.BlobPath); err != nil {
return fmt.Errorf("blob missing: %s: %w", e.BlobPath, err)
}
}
return nil
} Try / catch
err := resources.ZipResources(w, entries)
if err != nil {
if strings.Contains(err.Error(), "failed to stat blob") {
log.Error("blob missing; refusing to send truncated archive", "err", err)
// respond 500 BEFORE any bytes are written to w
return err
}
return err
} Prevention
- Pre-commit blobs before listing them as downloadable resources
- Use a single canonical hash encoding (lowercase hex) everywhere
- Coordinate GC/retention jobs so blobs aren't pruned while archives are being built
- Check blob existence before sending any HTTP 200 — the stream cannot report errors afterward
When it happens
Trigger: Calling ZipResources with a ZipEntry whose BlobPath doesn't exist — the .blob file was never committed, was cleaned up, or the path was constructed with the wrong hash/directory. Raised via resource-download flows.
Common situations: Content-addressed store pruned by a GC job between listing resources and downloading the archive; hash computed differently at commit vs. lookup (e.g. uppercase hex vs lowercase); blob directory misconfigured so entries point at the wrong root.
Related errors
- failed to commit blob %s: %w
- blob for resource %q not found on disk
- failed to get flow screenshot: %w
- failed to get absolute path: %w
- failed to create tmp directory: %w
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/11f31c47265a1109.
Report an issue: GitHub.