docker/compose · error

blob %s size mismatch: expected %d bytes, got %d

Error message

blob %s size mismatch: expected %d bytes, got %d

What it means

GetBlob deliberately reads descriptor.Size+1 bytes so it can detect both truncation and inflation: if the number of bytes actually read differs from the descriptor's declared size, the registry served a body that does not match its own manifest, and the blob is rejected before any integrity hashing. This guards against rogue or buggy registries causing wrong-size data (and, with the +1 cap, unbounded allocation).

Source

Thrown at internal/oci/resolver.go:120

// blob content must be fetched directly from the blobs endpoint.
func GetBlob(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor spec.Descriptor) ([]byte, error) {
	fetcher, err := resolver.Fetcher(ctx, ref.String())
	if err != nil {
		return nil, fmt.Errorf("creating fetcher for %s: %w", ref, err)
	}
	fetch, err := fetcher.Fetch(ctx, descriptor)
	if err != nil {
		return nil, fmt.Errorf("fetching blob %s: %w", descriptor.Digest, err)
	}
	defer func() { _ = fetch.Close() }()
	// bound the read by the declared size so a rogue registry can't cause
	// unbounded allocation; the extra byte detects oversized responses.
	content, err := io.ReadAll(io.LimitReader(fetch, descriptor.Size+1))
	if err != nil {
		return nil, fmt.Errorf("reading blob %s: %w", descriptor.Digest, err)
	}
	if int64(len(content)) != descriptor.Size {
		return nil, fmt.Errorf("blob %s size mismatch: expected %d bytes, got %d", descriptor.Digest, descriptor.Size, len(content))
	}
	// GetBlob bypasses containerd's content store, so integrity must be
	// checked here before callers write the bytes to disk.
	if err := descriptor.Digest.Validate(); err != nil {
		return nil, fmt.Errorf("invalid digest %s: %w", descriptor.Digest, err)
	}
	if actual := descriptor.Digest.Algorithm().FromBytes(content); actual != descriptor.Digest {
		return nil, fmt.Errorf("blob digest mismatch: expected %s, got %s", descriptor.Digest, actual)
	}
	return content, nil
}

func Copy(ctx context.Context, resolver remotes.Resolver, image reference.Named, named reference.Named) (spec.Descriptor, error) {
	src, desc, err := resolver.Resolve(ctx, image.String())
	if err != nil {
		return spec.Descriptor{}, err
	}
	if desc.Annotations == nil {

View on GitHub (pinned to ddc4b044b6)

Solutions

  1. Re-pull the artifact; if a proxy/CDN is in play, purge or bypass its cache.
  2. Re-push the artifact from the source so manifest sizes match actual blobs.
  3. Verify manually: compare Content-Length of GET /v2/<repo>/blobs/<digest> to descriptor.Size.
  4. Report persistent size skew to the registry operator — the manifest and blob store disagree.
Defensive patterns

Strategy: validation

Validate before calling

// trust, but verify: cross-check descriptor sizes against the registry
// HEAD /v2/<repo>/blobs/<digest> -> Content-Length must equal descriptor.Size
if contentLength != descriptor.Size {
    return fmt.Errorf("registry size %d != manifest size %d; registry state inconsistent", contentLength, descriptor.Size)
}

Try / catch

if int64(len(content)) != descriptor.Size {
    return nil, fmt.Errorf("blob %s size mismatch: expected %d bytes, got %d", descriptor.Digest, descriptor.Size, len(content))
    // never use or persist mismatched bytes; re-pull or re-push
}

Prevention

When it happens

Trigger: Registry returns fewer bytes than descriptor.Size (truncated blob, streaming bug) or more (pad/injection, wrong blob served, descriptor from a different manifest). Triggered by the comparison int64(len(content)) != descriptor.Size.

Common situations: Registry garbage collection raced with the pull; a caching proxy served a partial body; hand-crafted manifests with wrong layer sizes; registries with buggy range-request handling.

Related errors


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/30e00d5b124603df. Report an issue: GitHub.