Summary
RegistryClient blob push to AWS ECR can fail with HTTP 416 and error code BLOB_UPLOAD_INVALID when a large blob upload is interrupted or retried. ECR reports that the client restarted from byte 0 while the upload session already had bytes committed.
This is the underlying implementation issue tracked upstream in the container CLI repo: apple/container#1895
Steps to reproduce
Reproducible via container image push (which uses this library). Minimal flow:
aws ecr get-login-password --region <region> \
| container registry login --username AWS --password-stdin \
<account-id>.dkr.ecr.<region>.amazonaws.com
container build \
--platform linux/amd64,linux/arm64 \
-t <account-id>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag> \
.
container image push <account-id>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>
Most likely with large multi-arch images when a blob upload is partially sent then retried.
Current behavior
Example error:
416 Range Not Satisfiable
{"errors":[{"code":"BLOB_UPLOAD_INVALID","message":"First byte of the layer part is 0 instead of 11304960"}]}
Suspected root cause
Sources/ContainerizationOCI/Client/RegistryClient+Push.swift
Blob push flow:
POST /v2/<name>/blobs/uploads/ → obtain upload session URL
- Monolithic
PUT of entire blob to .../blobs/uploads/<uuid>?digest=sha256:...
The code passes a bodyClosure that resets the stream on retry:
// We have to pass a body closure rather than a body to reset the stream when retrying.
let bodyClosure = {
let stream = try streamGenerator()
return HTTPClientRequest.Body.stream(stream, length: .known(descriptor.size))
}
try await request(components: components, method: .PUT, bodyClosure: bodyClosure, ...)
Sources/ContainerizationOCI/Client/RegistryClient.swift
The generic request() loop retries by re-invoking bodyClosure() (stream from byte 0) against the same request URL:
while true {
request.body = try bodyClosure()
let _response = try await client.execute(request, ...)
// retries on 5xx (shouldRetry) and network errors
continue
}
For blob uploads, if PUT partially succeeds then retries (network error or generic retry), the registry expects continuation from the last committed byte, not a restart from byte 0.
Similar issues in other OCI clients:
Expected behavior
On blob upload failure or BLOB_UPLOAD_INVALID:
- Abandon the stale upload session and issue a fresh
POST /blobs/uploads/, then retry the full blob; or
- Resume via distribution-spec chunked upload (
GET upload status + PATCH with Content-Range).
Generic request() retry must not restart a blob PUT from byte 0 on the same upload session URL.
Suggested fix
- Refactor
push() in RegistryClient+Push.swift to separate initiateBlobUpload() and uploadBlob().
- Wrap blob upload in an outer retry loop; on
BLOB_UPLOAD_INVALID / HTTP 416, always start a new upload session (POST).
- Disable or bypass generic
request() retry for blob PUT, or pass retryOptions: nil for that call.
- Add tests in
Tests/ContainerizationOCITests/ simulating partial upload + 416 → successful retry with new session.
- (Optional) Implement
PATCH + Content-Range resume per the distribution spec.
Related
Summary
RegistryClientblob push to AWS ECR can fail with HTTP 416 and error codeBLOB_UPLOAD_INVALIDwhen a large blob upload is interrupted or retried. ECR reports that the client restarted from byte 0 while the upload session already had bytes committed.This is the underlying implementation issue tracked upstream in the
containerCLI repo: apple/container#1895Steps to reproduce
Reproducible via
container image push(which uses this library). Minimal flow:Most likely with large multi-arch images when a blob upload is partially sent then retried.
Current behavior
Example error:
Suspected root cause
Sources/ContainerizationOCI/Client/RegistryClient+Push.swiftBlob push flow:
POST /v2/<name>/blobs/uploads/→ obtain upload session URLPUTof entire blob to.../blobs/uploads/<uuid>?digest=sha256:...The code passes a
bodyClosurethat resets the stream on retry:Sources/ContainerizationOCI/Client/RegistryClient.swiftThe generic
request()loop retries by re-invokingbodyClosure()(stream from byte 0) against the same request URL:For blob uploads, if
PUTpartially succeeds then retries (network error or generic retry), the registry expects continuation from the last committed byte, not a restart from byte 0.Similar issues in other OCI clients:
Expected behavior
On blob upload failure or
BLOB_UPLOAD_INVALID:POST /blobs/uploads/, then retry the full blob; orGETupload status +PATCHwithContent-Range).Generic
request()retry must not restart a blobPUTfrom byte 0 on the same upload session URL.Suggested fix
push()inRegistryClient+Push.swiftto separateinitiateBlobUpload()anduploadBlob().BLOB_UPLOAD_INVALID/ HTTP 416, always start a new upload session (POST).request()retry for blobPUT, or passretryOptions: nilfor that call.Tests/ContainerizationOCITests/simulating partial upload + 416 → successful retry with new session.PATCH+Content-Rangeresume per the distribution spec.Related