{"record":{"id":"11f31c47265a1109","repo":"vxcontrol/pentagi","slug":"failed-to-stat-blob-s-w","errorCode":null,"errorMessage":"failed to stat blob %s: %w","messagePattern":"failed to stat blob (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":500,"severity":"error","filePath":"backend/pkg/resources/resources.go","lineNumber":331,"sourceCode":"\t\t// Already exists — remove tmp and consider success.\n\t\tos.Remove(tmpPath)\n\t\treturn nil\n\t}\n\tif err := os.Rename(tmpPath, dest); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit blob %s: %w\", hash, err)\n\t}\n\treturn nil\n}\n\n// ZipResources writes a ZIP archive to w containing all entries in files.\n// Each ZipEntry maps a .blob file on disk to a path inside the archive.\nfunc ZipResources(w io.Writer, entries []ZipEntry) (err error) {\n\t// The streaming HTTP caller commits its 200 status on the first byte written,\n\t// so a missing blob must be caught before then, or the client gets a truncated\n\t// archive under 200. Stat all blobs up front; don't fold into the write loop.\n\tfor _, e := range entries {\n\t\tif _, statErr := os.Lstat(e.BlobPath); statErr != nil {\n\t\t\treturn fmt.Errorf(\"failed to stat blob %s: %w\", e.BlobPath, statErr)\n\t\t}\n\t}\n\n\tzw := zip.NewWriter(w)\n\tdefer func() {\n\t\tif cerr := zw.Close(); err == nil {\n\t\t\terr = cerr\n\t\t}\n\t}()\n\n\tfor _, e := range entries {\n\t\tinfo, err := os.Lstat(e.BlobPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to stat blob %s: %w\", e.BlobPath, err)\n\t\t}\n\t\tif !info.Mode().IsRegular() {\n\t\t\tcontinue\n\t\t}","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/resources/resources.go#L313-L349","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nblobPath := filepath.Join(root, strings.ToUpper(hash)) // wrong case\n// after\nblobPath := filepath.Join(root, hex.EncodeToString(sum)) // lowercase, matches CommitBlob","handlingStrategy":"validation","validationCode":"func validateZipEntries(entries []resources.ZipEntry) error {\n\tfor _, e := range entries {\n\t\tif _, err := os.Lstat(e.BlobPath); err != nil {\n\t\t\treturn fmt.Errorf(\"blob missing: %s: %w\", e.BlobPath, err)\n\t\t}\n\t}\n\treturn nil\n}","typeGuard":null,"tryCatchPattern":"err := resources.ZipResources(w, entries)\nif err != nil {\n\tif strings.Contains(err.Error(), \"failed to stat blob\") {\n\t\tlog.Error(\"blob missing; refusing to send truncated archive\", \"err\", err)\n\t\t// respond 500 BEFORE any bytes are written to w\n\t\treturn err\n\t}\n\treturn err\n}","preventionTips":["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"],"tags":["filesystem","zip","missing-file","storage"],"backgroundTag":"missing-blob-file","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}