Skip to content

[Bug]: RegistryClient blob push retry restarts upload from byte 0, causing ECR BLOB_UPLOAD_INVALID (416) #790

Description

@sadysnaat

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:

  1. POST /v2/<name>/blobs/uploads/ → obtain upload session URL
  2. 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:

  1. Abandon the stale upload session and issue a fresh POST /blobs/uploads/, then retry the full blob; or
  2. 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

  1. Refactor push() in RegistryClient+Push.swift to separate initiateBlobUpload() and uploadBlob().
  2. Wrap blob upload in an outer retry loop; on BLOB_UPLOAD_INVALID / HTTP 416, always start a new upload session (POST).
  3. Disable or bypass generic request() retry for blob PUT, or pass retryOptions: nil for that call.
  4. Add tests in Tests/ContainerizationOCITests/ simulating partial upload + 416 → successful retry with new session.
  5. (Optional) Implement PATCH + Content-Range resume per the distribution spec.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions