diff --git a/internal/builder/mirror.go b/internal/builder/mirror.go index 83307fe..f5d5fb7 100644 --- a/internal/builder/mirror.go +++ b/internal/builder/mirror.go @@ -186,7 +186,7 @@ func (r Registry) MirrorImage(ctx context.Context, from, repository string) (str // on the first merge that rebuilt a whole catalogue (2026-09-28), and every module whose base // lives there failed on a copy it did not need. if strings.HasPrefix(where.reference, "sha256:") { - held, err := r.has(ctx, "http://"+r.Address+"/v2/"+repository+"/manifests/"+where.reference) + held, err := r.has(ctx, "http://"+r.Address+"/v2/"+repository+"/manifests/"+where.reference, manifestAccept) if err != nil { return "", fmt.Errorf("asking %s whether it holds %s: %w", r.Address, from, err) } diff --git a/internal/builder/mirror_test.go b/internal/builder/mirror_test.go index 7f8adbd..9a17b18 100644 --- a/internal/builder/mirror_test.go +++ b/internal/builder/mirror_test.go @@ -104,6 +104,14 @@ func (m *theMeshsRegistry) handler() http.Handler { defer m.mu.Unlock() switch { case r.Method == http.MethodHead && strings.Contains(r.URL.Path, "/manifests/"): + // **As strictly as a real registry.** A manifest is answered only in a media type the + // caller named; a request with no Accept is answered as if nothing were there. The fake + // used to answer regardless, which is why it could not catch a check that asked without + // one — and the mesh copied every base again (2026-09-28). + if !strings.Contains(r.Header.Get("Accept"), "manifest") && !strings.Contains(r.Header.Get("Accept"), "index") { + w.WriteHeader(http.StatusNotFound) + return + } if _, ok := m.manifests[r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]]; ok { w.WriteHeader(http.StatusOK) } else { diff --git a/internal/builder/registry.go b/internal/builder/registry.go index 481ce53..fd277d0 100644 --- a/internal/builder/registry.go +++ b/internal/builder/registry.go @@ -122,11 +122,23 @@ func (r Registry) PublishArchive(ctx context.Context, repository string, body [] return final, nil } -func (r Registry) has(ctx context.Context, url string) (bool, error) { +// has is whether this registry already holds what is at that URL. +// +// **A manifest HEAD must say what it accepts.** A registry answers a manifest request only in a media +// type the caller named, and a bare HEAD — no Accept at all — is answered 404 for a manifest it holds +// perfectly well. Measured against the mesh's own registry (2026-09-28): the same digest answered 200 +// with the manifest media types and 404 without them, so a check written without them concluded the +// registry held nothing, copied every base again, and exhausted the public hub's pull limit. A blob +// needs no Accept, which is why this went unnoticed: the same helper was right for blobs and wrong +// for manifests. +func (r Registry) has(ctx context.Context, url string, accept ...string) (bool, error) { request, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) if err != nil { return false, err } + for _, media := range accept { + request.Header.Set("Accept", media) + } response, err := r.client().Do(request) if err != nil { return false, fmt.Errorf("cannot reach the registry at %s: %w", r.Address, err)