fix(oci): force-stream manifest payloads to serve stored media type
Why
In DELIVERY_MODE_REDIRECT, a manifest GET returns a 307 to a signed storage URL that serves the payload as application/octet-stream, dropping the stored media type. containers/image clients (skopeo, podman, CRI-O) read the response Content-Type to detect the manifest schema, so pulls fail with unsupported schema version 2. Push and DELIVERY_MODE_PROXY pulls are unaffected: proxy mode already streams the payload with the stored media type. The OCI conformance suite passes in redirect mode, so real clients hit a gap the suite misses.
What
Force-stream manifest payloads so AR serves them with the stored container_manifests.media_type. Blobs keep redirecting. The fix adds storage.WithForceStream() to the one manifest-payload OpenBlob call in GetManifest, so the storage layer serves the bytes directly instead of signing a URL. This matches the Container Registry, which proxies manifests and redirects only blobs.
Reviewer-facing behavior change: under DELIVERY_MODE_REDIRECT, a manifest GET now returns 200 with the payload instead of a 307. Manifest payloads (≤4 MB, usually KB) now stream through AR, a negligible bandwidth change. The blob_download_bytes{delivery} label for manifest GETs moves from redirect to stream, which needs external Grafana and runbook updates (no in-repo dashboard keys reference it). The now-unreachable serveManifestRedirect is removed, and the S12 Manifest Pull wording is corrected to match invariant 5.
Test plan
TestGetManifest_GET_ForcesStreamasserts the manifest GET resolvesForceStream==true, verified to fail against the pre-fix code.- Existing handler tests assert the manifest GET returns the stored media type as
Content-Type. - The storage force-stream contract is already regression-locked in
pg_open_blob_integration_test.go. conformance:ociruns on the MR pipeline. The fix moves manifests toward conformance (200 plus the stored Content-Type).
| Acceptance criterion | Test / evidence |
|---|---|
Manifest GET folds OpenBlob opts to ForceStream==true |
TestGetManifest_GET_ForcesStream (new; fails pre-fix) |
Manifest GET returns 200 + body + Content-Type = stored media type |
existing handler content-type tests + the storage force-stream contract |
| Blob GET still redirects (307) under redirect mode | TestBlobGet_RedirectReturns307 (unchanged) |
| No dead redirect code remains | serveManifestRedirect removed; grep confirms zero references |
| S12 states manifests always stream | docs/specs/S12-container-oci-hosted.md edits |
| Package tests, lint, OCI conformance pass | local build/vet/test + CI conformance:oci |
Related to #362 (closed)
Context for LLM agents
Design: chose Option B (force-stream manifests) over Option A (a response-content-type override on the signed URL). Option A works on GCS and S3 natively but requires extending the URLFor(ctx, path) storage interface, both drivers, three URL middleware decorators (CloudFront, Cloud CDN, URL cache), and CDN query-parameter forwarding config. That is a wide, signing-correctness-sensitive blast radius for a severity::2 fix. Option B is one functional line against an already-tested path (effectiveMode returns proxy when ForceStream, so URLFor is skipped), matches the Container Registry reference impl, and fits ADR-005 (manifests are ≤4 MB, so redirect saved negligible bandwidth).
Non-goals: no change to blob delivery (blobs still redirect); no per-call content-type override added to the storage interface; no ADR-005 edit (handbook-side, tracked separately). CDN offload for manifests is intentionally given up; monitor manifest-GET latency and storage Reader rate post-deploy.