vxcontrol/pentagi · error

failed to compute MD5: %w

Error message

failed to compute MD5: %w

What it means

ComputeFileMD5 (resources.go:75) reads an io.Reader to EOF and returns the lowercase hex MD5 digest. The error 'failed to compute MD5: %w' is returned when io.Copy fails while streaming bytes into the md5 hash. It wraps the underlying read error, so the root cause (disk failure, broken pipe, permission error on a file-backed reader) is available via errors.Unwrap or errors.As/Is.

Source

Thrown at backend/pkg/resources/resources.go:75

	if !IsValidBlobHash(cleanHash) {
		return filepath.Join(ResourcesDir(dataDir), invalidBlobHashFileName)
	}
	return filepath.Join(ResourcesDir(dataDir), cleanHash+".blob")
}

// EnsureResourcesDir creates the resources storage directory if it does not exist.
func EnsureResourcesDir(dataDir string) error {
	if err := os.MkdirAll(ResourcesDir(dataDir), 0755); err != nil {
		return fmt.Errorf("failed to create resources directory: %w", err)
	}
	return nil
}

// ComputeFileMD5 reads r to EOF and returns the lowercase hex MD5 digest.
func ComputeFileMD5(r io.Reader) (string, error) {
	h := md5.New()
	if _, err := io.Copy(h, r); err != nil {
		return "", fmt.Errorf("failed to compute MD5: %w", err)
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

// IsValidBlobHash reports whether hash is a hex-encoded MD5 digest.
func IsValidBlobHash(hash string) bool {
	if len(hash) != md5.Size*2 {
		return false
	}
	for _, r := range hash {
		if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
			continue
		}
		return false
	}
	return true
}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Inspect the wrapped cause with errors.Unwrap(err) or %v of the error to find the real I/O failure
  2. Re-check the source file exists and is readable (os.Stat, permissions) before hashing
  3. For streams, retry the read/hash from the start of the stream; partial hashing is not recoverable
  4. If the reader can legitimately fail mid-copy, wrap it with buffering or read it fully into memory only if size permits

Example fix

// before
hash, err := resources.ComputeFileMD5(f) // f may be stale/closed
if err != nil { return err }
// after
if _, err := f.Stat(); err != nil { return fmt.Errorf("source unavailable: %w", err) }
if _, err := f.Seek(0, io.SeekStart); err != nil { return err }
hash, err := resources.ComputeFileMD5(f)
if err != nil { return fmt.Errorf("hashing failed: %w", err) }
Defensive patterns

Strategy: try-catch

Validate before calling

if f, ok := r.(*os.File); ok {
    if _, err := f.Stat(); err != nil {
        return fmt.Errorf("source unreadable: %w", err)
    }
}

Type guard

var _ = func(r io.Reader) bool { return r != nil }

Try / catch

hash, err := resources.ComputeFileMD5(r)
if err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) { /* handle open/read failure on path */ }
    return fmt.Errorf("md5 compute failed: %w", err)
}

Prevention

When it happens

Trigger: Calling ComputeFileMD5 with a reader whose underlying source fails during read: an *os.File opened on a removed or unreadable file, a network/HTTP body that drops mid-stream, or a bytes.Reader is fine but a custom reader returning an error.

Common situations: Hashing a file that was truncated or deleted between open and read; hashing an upload stream that was interrupted; permission changes on the source file; storage device errors in Docker volume backends.

Related errors


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