fix(maven): state cache directives on every read response
Summary
Every Maven read response left the origin with no Cache-Control and no Vary. An edge that caches by file extension by default then stored the .jar 302 (signed storage URL) and the remote cold-fill 200 (the artifact bytes) under a key that ignores the credential, and replayed one principal's authorized response to another for the edge's default TTL. .pom, .tgz and extension-less blob paths were not on that default list, which is why the symptom looked jar-only; the missing directive covered the whole Maven read surface.
This MR makes every Maven read shape state its own cacheability, the rule S15 already fixes for npm remote:
- every successful
GETandHEADbody shape, HEAD,304and checksum sidecar, hosted and remote alike, carriesVary: AuthorizationandCache-Control: private; - the
302storage redirect carriesVary: AuthorizationandCache-Control: no-store, because its signedLocationexpires and a stored redirect would replay a dead link even from a private cache; - the remote artifact cold-fill
200carries the sameprivatedirective as every other body shape.
The directive is unconditional: it does not vary by repository visibility, because closed-beta authorization treats every repository as private (S09, ADR-021) and a stored public copy would outlive a later flip to private. Bare private carries no lifetime on purpose: no Maven response sets Last-Modified, so a private cache has nothing to derive heuristic freshness from and revalidates through the ETag; the cold-fill 200 carries no validator, so a stored copy costs a full re-fetch.
Two unexported helpers in internal/format/maven/download.go stage the two header pairs at each success write site rather than at the dispatch entry, so error responses are byte-identical to before. The six conditional-match 304 arms share one writeNotModified helper, so a new arm cannot ship without the directive.
The spec amendment rides in the same MR: S10's API contract and acceptance criteria gain the rule, S10's Security Considerations bullet that deferred "redirect-cache hygiene" to S06 (which never stated it) now points at the contract, and S14 inherits it with the cold-fill header subset amended to match.
Out of scope, by decision
- Error responses (
404,416,422,5xx) keep their current headers. The read-path404is heuristically cacheable and the edge caches it too; that is #1131 (closed). - The npm hosted and OCI blob
307redirects still rely on header omission; #1084 tracks the OCI side. - A zone-side cache-bypass rule is defense in depth outside this repository.
Vary is stated for RFC-compliant intermediaries and for consistency with the npm surface; the property this MR relies on against the observed edge is Cache-Control: private / no-store, which that edge honors regardless of Vary.
Governing ADRs
- ADR-005 (Artifact Delivery Mode) and ADR-013 (Storage Backend Interaction) place CDN caching on the signed storage URL, whose signature the CDN validates at the edge. Neither speaks to caching of the registry's own redirect or body response in front of the origin; this change adds directives on that response and leaves the signed-URL path untouched. Conforms, no deviation.
- ADR-024 (Infrastructure Delivery) places a shared edge in front of the service. This change is what makes the service's responses safe behind it. Conforms.
Testing
TestHostedRead_EveryServeShapeIsCacheSeparated(internal/format/maven/cache_headers_test.go): hosted primary200proxy,302redirect,206, HEAD200, GET and HEAD304, sidecar200, HEAD200, GET and HEAD304. Asserts on the response snapshot a client receives, thatVaryis exactlyAuthorizationonce, and the single directive per shape.TestRemoteRead_EveryDirectServeShapeIsCacheSeparated(internal/format/maven/remote_cache_headers_test.go): the kind=2 arms a direct call reaches without a database: the artifact cold-fill stream200, sidecar synthesis200, HEAD200, GET and HEAD304, and the three row-derived304short-circuits.- One-line assertions in the existing integration tests for the remote fresh hit
200, redirect-delivery302, conditional304, artifact cold-fill200, metadata cold-fill200, and every sidecarGET/HEAD. - All 18 new unit subtests fail against the unchanged production code and pass with it.
- Ran locally:
go test ./internal/format/maven/andgo test -tags=integration ./internal/format/maven/(full package, against Colima),golangci-lint run ./internal/format/maven/and the--build-tags=integration --max-same-issues=0 --max-issues-per-linter=0 --uniq-by-line=falseform (no findings on touched lines),scripts/ci/check-comment-caps.sh --base origin/main, vale, markdownlint and lychee on the three edited documents. - e2e catalog:
docs/testing/e2e/maven.mdgainse2e.maven.consume.read-cache-headers.
The streamArtifactFill doc comment enumerates the content headers that function stages and was left unedited; the cache headers are staged beside them, not inside, so the comment is incomplete rather than false, and editing it would pull a 15-line block into the one-line unexported cap.
Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/1070 Closes #1133 (closed)