{"record":{"id":"c811f1d440e55581","repo":"theonedev/onedev","slug":"digest-invalid","errorCode":"DIGEST_INVALID","errorMessage":"Digest expected to finish blob upload","messagePattern":"Digest expected to finish blob upload","errorType":"http","errorClass":"ClientException","httpStatus":400,"severity":"error","filePath":"server-plugin/server-plugin-pack-container/src/main/java/io/onedev/server/plugin/pack/container/ContainerServlet.java","lineNumber":213,"sourceCode":"\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"PUT\": {\n\t\t\t\t\t\tif (packBlobService.getUploadFileSize(projectId, uuid) == -1)\n\t\t\t\t\t\t\tthrow new NotFoundException(ErrorCode.BLOB_UPLOAD_UNKNOWN);\n\t\t\t\t\t\tvar contentLength = request.getHeader(\"Content-Length\");\n\t\t\t\t\t\tif (contentLength != null) {\n\t\t\t\t\t\t\tvar parsedContentLength = parseLong(contentLength);\n\t\t\t\t\t\t\tif (parsedContentLength != 0) {\n\t\t\t\t\t\t\t\ttry (var is = request.getInputStream()) {\n\t\t\t\t\t\t\t\t\tpackBlobService.uploadBlob(projectId, uuid, is);\n\t\t\t\t\t\t\t\t} catch (IOException e) {\n\t\t\t\t\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar digestString = request.getParameter(\"digest\");\n\t\t\t\t\t\tif (digestString == null) {\n\t\t\t\t\t\t\tthrow new ClientException(SC_BAD_REQUEST, ErrorCode.DIGEST_INVALID,\n\t\t\t\t\t\t\t\t\t\"Digest expected to finish blob upload\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar digest = parseDigest(digestString);\n\t\t\t\t\t\tif (packBlobService.finishUpload(projectId, uuid, digest.getHash()) != null) {\n\t\t\t\t\t\t\tresponse.setStatus(SC_CREATED);\n\t\t\t\t\t\t\tresponse.setHeader(\"Location\", getBlobUrl(projectPath, repository, digestString));\n\t\t\t\t\t\t\tresponse.setHeader(\"Docker-Content-Digest\", digestString);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthrow new ClientException(SC_BAD_REQUEST, ErrorCode.DIGEST_INVALID,\n\t\t\t\t\t\t\t\t\t\"Invalid blob digest\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcase \"GET\": {\n\t\t\t\t\t\tvar uploadedSize = packBlobService.getUploadFileSize(projectId, uuid);\n\t\t\t\t\t\tif (uploadedSize == -1)\n\t\t\t\t\t\t\tthrow new NotFoundException(ErrorCode.BLOB_UPLOAD_UNKNOWN);","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/theonedev/onedev/blob/d44925c47c37992c828ea673a5f9620539bc3ff2/server-plugin/server-plugin-pack-container/src/main/java/io/onedev/server/plugin/pack/container/ContainerServlet.java#L195-L231","documentation":"Finalizing a chunked blob upload requires a POST to the upload URL with a digest query parameter identifying the complete blob. This error is thrown when the finalizing POST omits the digest parameter; the registry cannot verify what the assembled blob should hash to, so it rejects the request with 400 and code DIGEST_INVALID.","triggerScenarios":"POST /v2/<name>/blobs/uploads/<uuid>?digest=<digest> sent without the digest query parameter while uploadedSize > 0. Also: clients that send the digest as a header or body instead of a query parameter; URL-encoded clients dropping the query string on redirect.","commonSituations":"Custom scripts finishing an upload with plain POST (no ?digest=); clients following a 3xx redirect that strips the query string; hand-rolled OCI tooling missing the finalization step's parameter; API wrappers that build the finalize URL incorrectly.","solutions":["Send the finalize request as POST to the Location URL with ?digest=sha256:<hex> matching the full blob digest.","Compute the digest over the entire blob content (all chunks concatenated), not per-chunk digests.","Verify the URL retains its query string across redirects (configure the HTTP client to preserve it).","If the client cannot compute digests, switch to single-POST monolithic upload where the server computes it."],"exampleFix":"// before\npost(uploadLocation); // no digest\n// after\npost(uploadLocation + \"?digest=\" + URLEncoder.encode(digest, UTF_8));","handlingStrategy":"validation","validationCode":"const url = new URL(uploadLocation);\nif (!url.searchParams.get('digest')) {\n  url.searchParams.set('digest', fullBlobDigest); // sha256:<hex of entire blob>\n}\n// request finalization with url.toString()","typeGuard":null,"tryCatchPattern":"catch (err) {\n  if (err.status === 400 && err.code === 'DIGEST_INVALID' && /Digest expected/.test(err.message)) {\n    await finalizeUpload(uploadLocation, computeDigest(entireBlob));\n  } else throw err;\n}","preventionTips":["Always append ?digest= to the finalize POST URL.","Compute the digest over all chunks concatenated, not per chunk.","Preserve query strings across HTTP redirects.","Use monolithic single-POST upload if digest computation is impractical."],"tags":["oci-registry","blob-upload","digest","http-400"],"backgroundTag":"missing-required-argument","analyzedSha":"d44925c47c37992c828ea673a5f9620539bc3ff2","analyzedAt":"2026-09-06T07:18:27.995Z","contentChangedAt":"2026-09-06T07:18:27.995Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}