theonedev/onedev · error · ClientException

BLOB_UPLOAD_INVALID

BLOB_UPLOAD_INVALID

Error message

Invalid chunk range

What it means

This error is thrown during a chunked (PATCH) docker registry blob upload when the Content-Range header declares a start offset that does not match the number of bytes already uploaded to the server. The registry responds with 416 (Requested Range Not Satisfiable) and a Range header telling the client where to resume. It enforces the OCI distribution spec requirement that chunks be appended strictly in order with no gaps or overlaps.

Source

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

				});
			} else if ((matcher = compile("(.+)/([^/]+)/blobs/uploads/([^/]+)").matcher(pathInfo)).matches()) {
				var projectPath = matcher.group(1);
				var repository = matcher.group(2);
				var uuid = matcher.group(3);
				response.setHeader("Location", getUploadUrl(projectPath, repository, uuid));
				response.setHeader("Docker-Upload-UUID", uuid);
				var projectId = sessionService.call(() -> checkProject(projectPath, true).getId());
				switch (method) {
					case "PATCH": {
						var uploadedSize = packBlobService.getUploadFileSize(projectId, uuid);
						if (uploadedSize == -1)
							throw new NotFoundException(ErrorCode.BLOB_UPLOAD_UNKNOWN);
						var contentRange = request.getHeader("Content-Range");
						if (contentRange != null) {
							var chunkBegin = parseLong(substringBefore(contentRange, "-"));
							if (uploadedSize != chunkBegin) {
								response.setHeader("Range", "0-" + (uploadedSize - 1));
								throw new ClientException(SC_REQUESTED_RANGE_NOT_SATISFIABLE,
										ErrorCode.BLOB_UPLOAD_INVALID, "Invalid chunk range");
							}
						} else if (uploadedSize != 0) {
							throw new ClientException(SC_REQUESTED_RANGE_NOT_SATISFIABLE, ErrorCode.BLOB_UPLOAD_INVALID,
									"Content range header expected after first upload");
						}
						try (var is = request.getInputStream()) {
							uploadedSize += packBlobService.uploadBlob(projectId, uuid, is);
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
						response.setStatus(SC_ACCEPTED);
						response.setHeader("Range", "0-" + (uploadedSize - 1));
						break;
					}
					case "PUT": {
						if (packBlobService.getUploadFileSize(projectId, uuid) == -1)
							throw new NotFoundException(ErrorCode.BLOB_UPLOAD_UNKNOWN);

View on GitHub (pinned to d44925c47c)

Solutions

  1. Read the Range response header from the 416 reply and re-issue the PATCH with Content-Range starting at uploadedSize (end of server's Range).
  2. Do not upload chunks in parallel or out of order; append strictly sequentially.
  3. After any failure, issue a GET on the upload URL to learn the authoritative uploaded size before resuming.
  4. Reconfigure HTTP clients/proxies not to transparently retry PATCH requests with the original body/headers.

Example fix

// before
request.setHeader("Content-Range", "0-" + (file.length() - 1)); // always full range
// after
long uploaded = parseRangeEndFromResponse(lastResponse.getHeader("Range")) + 1;
request.setHeader("Content-Range", uploaded + "-" + (uploaded + chunk.length - 1));
Defensive patterns

Strategy: validation

Validate before calling

const range = res.headers.get('Range'); // '0-<uploadedSize-1>' on 416
const uploaded = range ? parseInt(range.split('-')[1], 10) + 1 : 0;
if (chunkStart !== uploaded) {
  chunkStart = uploaded; // re-align chunk offset before re-PATCH
}

Try / catch

catch (err) {
  if (err.status === 416 && err.code === 'BLOB_UPLOAD_INVALID') {
    const uploaded = getUploadStatus(uploadUrl); // GET returns Range
    resumeUploadFrom(uploaded);
  } else throw err;
}

Prevention

When it happens

Trigger: PATCH request to /v2/<name>/blobs/uploads/<uuid> whose Content-Range header (e.g. 'Content-Range: 500-999') has a begin offset != the bytes already stored (uploadedSize). Also fired when a client retries a chunk from the wrong offset after a network interruption, or sends a stale Content-Range after another chunk already landed.

Common situations: Docker/podman/kaniko pushes over flaky networks where a chunk is retried with a stale offset; parallel chunk uploads racing each other so offsets no longer match server state; custom OCI clients computing Content-Range from local file position instead of the server's returned Range header; proxy/load-balancer retrying a PATCH against a different backend.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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