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.
Solutions
- 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).
- Do not upload chunks in parallel or out of order; append strictly sequentially.
- After any failure, issue a GET on the upload URL to learn the authoritative uploaded size before resuming.
- 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
- Always derive Content-Range from the server's Range header, not local file offsets.
- Upload chunks strictly sequentially; never parallelize PATCHes to one session.
- After any failure, GET the upload URL to re-sync state before resuming.
- Disable transparent HTTP-client retries for PATCH requests.
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
- DIGEST_INVALID
- Invalid http method for blob upload
- DENIED
- Invalid http method for blob pull
- Invalid http method for manifest pull
AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06).
Data as JSON: /api/errors/df50c1d12bf05493.
Report an issue: GitHub.
Appendix: 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)