{"record":{"id":"6b2423d525bb55dd","repo":"goharbor/harbor","slug":"bad-request-6b2423","errorCode":"BAD_REQUEST","errorMessage":"require digest","messagePattern":"require digest","errorType":"validation","errorClass":"lib/errors.Error","httpStatus":400,"severity":"warning","filePath":"src/controller/blob/controller.go","lineNumber":223,"sourceCode":"\n\tassociated := make(map[string]bool, len(associatedBlobs))\n\tfor _, blob := range associatedBlobs {\n\t\tassociated[blob.Digest] = true\n\t}\n\n\tvar results []*blob.Blob\n\tfor _, blob := range blobs {\n\t\tif !associated[blob.Digest] {\n\t\t\tresults = append(results, blob)\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\nfunc (c *controller) Get(ctx context.Context, digest string, options ...Option) (*blob.Blob, error) {\n\tif digest == \"\" {\n\t\treturn nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessage(\"require digest\")\n\t}\n\n\topts := newOptions(options...)\n\n\tkeywords := make(map[string]any)\n\tif digest != \"\" {\n\t\tol := q.OrList{\n\t\t\tValues: []any{\n\t\t\t\tdigest,\n\t\t\t},\n\t\t}\n\t\tkeywords[\"digest\"] = &ol\n\t}\n\tif opts.ProjectID != 0 {\n\t\tkeywords[\"projectID\"] = opts.ProjectID\n\t}\n\tif opts.ArtifactDigest != \"\" {\n\t\tkeywords[\"artifactDigest\"] = opts.ArtifactDigest","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/goharbor/harbor/blob/7b2fd08cc568955cca339afeefab27372840d936/src/controller/blob/controller.go#L205-L241","documentation":"The blob controller's Get(ctx, digest) uses the digest string as the only lookup key (it builds an OrList on keywords[\"digest\"]). An empty digest cannot identify any blob record, so the controller rejects the call with BAD_REQUEST before it reaches the DAO layer. This is a guard against callers forwarding a missing/zero-value digest.","triggerScenarios":"Calling controller/blob Get with digest == \"\" - e.g. an API handler that forwards a missing :digest path/query parameter, or internal code (replication, scanning, GC) that reads a digest field that was never populated.","commonSituations":"Client calls a blob endpoint without the digest segment; upstream struct has an empty Digest because the manifest was fetched partially; code assumes an earlier layer already validated the digest.","solutions":["Pass the non-empty sha256 digest of the blob you want to fetch.","Validate the digest at the boundary (route handler / API client) before calling the controller, ideally with the standard digest regexp.","If the digest comes from another subsystem, debug why it arrives empty (nil manifest, truncated response) instead of retrying unchanged."],"exampleFix":"// before\nb, err := blobCtl.Get(ctx, digest) // digest == \"\"\n\n// after\nif digest == \"\" {\n    return liberrors.BadRequestError(errors.New(\"require digest\"))\n}\nb, err := blobCtl.Get(ctx, digest)","handlingStrategy":"validation","validationCode":"var digestRe = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`)\n\nfunc validDigest(d string) bool { return digestRe.MatchString(d) }\n\n// call before blobCtl.Get:\nif !validDigest(digest) {\n    return liberrors.BadRequestError(fmt.Errorf(\"require digest\"))\n}","typeGuard":"func isDigestable(s string) bool {\n    return len(s) > len(\"sha256:\") && strings.HasPrefix(s, \"sha256:\")\n}","tryCatchPattern":"if _, err := blobCtl.Get(ctx, digest); err != nil {\n    if liberrors.IsErr(err, liberrors.BadRequestCode) && err.Error() == \"require digest\" {\n        // caller bug: fix the digest source, do not retry\n    }\n    return err\n}","preventionTips":["Validate digest format at the API boundary with the standard regexp.","Never pass struct fields straight through - assert non-empty first.","Fail fast in route handlers for missing :digest path params."],"tags":["blob","validation","bad-request","go"],"backgroundTag":null,"analyzedSha":"7b2fd08cc568955cca339afeefab27372840d936","analyzedAt":"2026-08-16T00:00:10.961Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}