Container registry: multi-arch tags 500 on the legacy (non-database) path since 19.0.2

Summary

Since GitLab 19.0.2, the container registry UI and tag API return 500 for multi-arch tags when the registry runs on the legacy (non-database) path (database.enabled = false, or prefer-fallback mode). The UI shows "This image has no active tags" and "Something went wrong while fetching the repository list". The tag API (GET /projects/:id/registry/repositories/:rid/tags/:tag) returns 500. docker push and pull are unaffected.

This is a Rails-side regression, separate from the registry prefer-mode bug fixed in 19.0.2 (container-registry v4.40.1, fix(handlers): defer router init until metadata... (container-registry!2877 - merged) • Hayley Swimelar • 19.1). It surfaced in Container Registry becomes inaccessible after u... (#600955 - closed) • Hayley Swimelar • 19.1.

Root cause

The shared ACCEPTED_TYPES in lib/container_registry/base_client.rb gained manifest.list.v2+json and oci.image.index.v1+json, and lost the separate ACCEPTED_TYPES_RAW, in Backport of 'Geo: fix container repository sync... (!238384 - merged) • Douglas Barbosa Alexandre • 19.0.

ACCEPTED_TYPES is also the Accept header for the default faraday connection (base_client.rb:119), which the legacy path uses for every per-tag manifest fetch (repository_manifest, repository_tag_digest), not only Geo's faraday_raw. The wider Accept changes what the registry returns for a multi-arch tag:

  • Docker manifest list: the registry previously downgraded it to a single-platform schema2 manifest (rewriteManifestList, via the registry's asymmetric Accept negotiation), which the client parsed. It now returns the real fat manifest. configure_connection does not register manifest.list.v2+json as a JSON response type, so the body returns as an unparsed String and ContainerRegistry::Tag#layers runs (manifest['layers'] || manifest['fsLayers']).map, i.e. nil.map -> NoMethodError -> 500. It is reached through total_size and the GraphQL totalSize field.
  • OCI image index: previously returned 404 (ManifestUnknown) and the tag was treated as having no manifest. It now parses to a Hash with no layers, hitting the same nil.map.

Scope

19.0.2+ installs on the legacy v2 enumeration path with multi-arch images:

Metadata-database installs are unaffected. The v1 GitlabApiClient returns tag size and digest from the API and never fetches or parses manifests (container_repository.rb:347,364).

Proposed fix

Give faraday_raw its own wide Accept again (restore ACCEPTED_TYPES_RAW) and keep the default faraday connection image-only. This preserves the Geo OCI-index fix and lets the registry keep downgrading manifest lists to a single-platform manifest on the legacy path, which is the intended v2-by-tag behavior. Fully parsing lists and indexes in the legacy client is not worth it: a real size needs per-platform manifest fan-out, the slow enumeration the database API already replaces, on a path being retired.

Independent hardening: Tag#layers should guard against a nil or unexpected manifest (return []) so a single odd tag cannot 500 the whole GraphQL query.

Steps to reproduce

  1. GitLab 19.0.2+ with the container registry on the legacy path (registry['database']['enabled'] = false, or prefer-fallback with filesystem metadata).
  2. Push a multi-arch image, e.g. docker buildx build --platform linux/amd64,linux/arm64 --push <ref>.
  3. Open the project container registry, or call GET /projects/:id/registry/repositories/:rid/tags/:tag.
  4. Result: 500 and an empty tag list.
Edited by Hayley Swimelar