{"record":{"id":"df50c1d12bf05493","repo":"theonedev/onedev","slug":"blob-upload-invalid","errorCode":"BLOB_UPLOAD_INVALID","errorMessage":"Invalid chunk range","messagePattern":"Invalid chunk range","errorType":"http","errorClass":"ClientException","httpStatus":416,"severity":"error","filePath":"server-plugin/server-plugin-pack-container/src/main/java/io/onedev/server/plugin/pack/container/ContainerServlet.java","lineNumber":181,"sourceCode":"\t\t\t\t});\n\t\t\t} else if ((matcher = compile(\"(.+)/([^/]+)/blobs/uploads/([^/]+)\").matcher(pathInfo)).matches()) {\n\t\t\t\tvar projectPath = matcher.group(1);\n\t\t\t\tvar repository = matcher.group(2);\n\t\t\t\tvar uuid = matcher.group(3);\n\t\t\t\tresponse.setHeader(\"Location\", getUploadUrl(projectPath, repository, uuid));\n\t\t\t\tresponse.setHeader(\"Docker-Upload-UUID\", uuid);\n\t\t\t\tvar projectId = sessionService.call(() -> checkProject(projectPath, true).getId());\n\t\t\t\tswitch (method) {\n\t\t\t\t\tcase \"PATCH\": {\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);\n\t\t\t\t\t\tvar contentRange = request.getHeader(\"Content-Range\");\n\t\t\t\t\t\tif (contentRange != null) {\n\t\t\t\t\t\t\tvar chunkBegin = parseLong(substringBefore(contentRange, \"-\"));\n\t\t\t\t\t\t\tif (uploadedSize != chunkBegin) {\n\t\t\t\t\t\t\t\tresponse.setHeader(\"Range\", \"0-\" + (uploadedSize - 1));\n\t\t\t\t\t\t\t\tthrow new ClientException(SC_REQUESTED_RANGE_NOT_SATISFIABLE,\n\t\t\t\t\t\t\t\t\t\tErrorCode.BLOB_UPLOAD_INVALID, \"Invalid chunk range\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (uploadedSize != 0) {\n\t\t\t\t\t\t\tthrow new ClientException(SC_REQUESTED_RANGE_NOT_SATISFIABLE, ErrorCode.BLOB_UPLOAD_INVALID,\n\t\t\t\t\t\t\t\t\t\"Content range header expected after first upload\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry (var is = request.getInputStream()) {\n\t\t\t\t\t\t\tuploadedSize += packBlobService.uploadBlob(projectId, uuid, is);\n\t\t\t\t\t\t} catch (IOException e) {\n\t\t\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresponse.setStatus(SC_ACCEPTED);\n\t\t\t\t\t\tresponse.setHeader(\"Range\", \"0-\" + (uploadedSize - 1));\n\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);","sourceCodeStart":163,"sourceCodeEnd":199,"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#L163-L199","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nrequest.setHeader(\"Content-Range\", \"0-\" + (file.length() - 1)); // always full range\n// after\nlong uploaded = parseRangeEndFromResponse(lastResponse.getHeader(\"Range\")) + 1;\nrequest.setHeader(\"Content-Range\", uploaded + \"-\" + (uploaded + chunk.length - 1));","handlingStrategy":"validation","validationCode":"const range = res.headers.get('Range'); // '0-<uploadedSize-1>' on 416\nconst uploaded = range ? parseInt(range.split('-')[1], 10) + 1 : 0;\nif (chunkStart !== uploaded) {\n  chunkStart = uploaded; // re-align chunk offset before re-PATCH\n}","typeGuard":null,"tryCatchPattern":"catch (err) {\n  if (err.status === 416 && err.code === 'BLOB_UPLOAD_INVALID') {\n    const uploaded = getUploadStatus(uploadUrl); // GET returns Range\n    resumeUploadFrom(uploaded);\n  } else throw err;\n}","preventionTips":["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."],"tags":["oci-registry","blob-upload","http-416","content-range"],"backgroundTag":"invalid-argument-value","analyzedSha":"d44925c47c37992c828ea673a5f9620539bc3ff2","analyzedAt":"2026-09-06T07:18:27.995Z","contentChangedAt":"2026-09-06T07:18:27.995Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}