theonedev/onedev · error · BadRequestException

Invalid http method for blob pull: ${method}

Error message

Invalid http method for blob pull: ${method}

What it means

The blob pull endpoint supports GET, HEAD, and (explicitly rejected) DELETE; any other method is rejected with this BadRequestException. DELETE on a blob returns 405 UNSUPPORTED. This guards the read-only nature of the blob resource beyond the method-not-allowed path.

Source

Thrown at server-plugin/server-plugin-pack-container/src/main/java/io/onedev/server/plugin/pack/container/ContainerServlet.java:271

						var hash = digest.getHash();
						PackBlob packBlob;
						if ((packBlob = packBlobService.checkPackBlob(project.getId(), hash)) != null) {
							response.setStatus(SC_OK);	
							response.setHeader("Content-Length", String.valueOf(packBlob.getSize()));
							response.setHeader("Docker-Content-Digest", digestString);
							return new Pair<>(packBlob.getProject().getId(), packBlob.getSha256Hash());
						} else {
							throw new NotFoundException(ErrorCode.BLOB_UNKNOWN);
						}
					});
					if (method.equals("GET")) {
						packBlobService.downloadBlob(packBlobInfo.getLeft(), packBlobInfo.getRight(),
								response.getOutputStream());
					}
				} else if (method.equals("DELETE")) {
					throw new ClientException(SC_METHOD_NOT_ALLOWED, ErrorCode.UNSUPPORTED);
				} else {
					throw new BadRequestException("Invalid http method for blob pull: " + method);
				}
			} else if ((matcher = compile("(.+)/([^/]+)/manifests/([^/]+)").matcher(pathInfo)).matches()) {
				var projectPath = matcher.group(1);
				var repository = matcher.group(2);
				var reference = matcher.group(3);
				switch (method) {
					case "PUT":
						var projectId = sessionService.call(() -> checkProject(projectPath, true).getId());
						var baos = new ByteArrayOutputStream();
						try (var is = request.getInputStream()) {
							var copied = copyWithMaxSize(is, baos, MAX_MANIFEST_SIZE);
							if (copied == -1)
								throw new ClientException(SC_NOT_ACCEPTABLE, ErrorCode.DENIED, "Manifest exceeds maximum size: " + MAX_MANIFEST_SIZE);
						}

						var bytes = baos.toByteArray();
						String hash;
						if (isTag(reference)) {

View on GitHub (pinned to d44925c47c)

Solutions

  1. Upload blobs via POST /v2/<name>/blobs/uploads/ then PATCH/PUT on the returned session URL; never PUT to /blobs/<digest>.
  2. Fetch blobs with GET (or HEAD for existence checks) on /v2/<name>/blobs/<digest>.
  3. Do not attempt DELETE on blobs; blobs are garbage-collected server-side.
  4. Update the client to this registry's supported method set (GET/HEAD for pull).

Example fix

// before
PUT /v2/app/blobs/sha256:abc...  body=@layer.tgz
// after
loc = POST /v2/app/blobs/uploads/  -> Location
PATCH loc (chunk) ; PUT loc?digest=sha256:abc...
Defensive patterns

Strategy: validation

Validate before calling

if (!['GET','HEAD'].includes(method)) {
  throw new Error(`Blob pull supports GET/HEAD only, got ${method}`);
}

Prevention

When it happens

Trigger: Any method other than GET or HEAD sent to /v2/<name>/blobs/<digest> — e.g. PUT attempting to push a blob at its final URL, POST, or OPTIONS. PUT on the blob URL is a common mistake because some registries tolerate monolithic PUT; OneDev requires the POST-initiated upload flow.

Common situations: Clients implementing the monolithic PUT upload variant (POST then PUT to /blobs/<digest>) which this registry does not support; generic REST clients defaulting to POST; tooling probing the blob endpoint with OPTIONS.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06). Data as JSON: /api/errors/024166b50f261654. Report an issue: GitHub.