fix: state Cache-Control: no-store on every error envelope
Summary
Every error response left the origin with no Cache-Control, so an edge retained the first 404 for a mutable artifact URL and kept answering it after the artifact was published at that coordinate (staging, ar-registry, 2026-09-02). The same missing directive let a cached 302 keep serving a deleted artifact until the entry lapsed (see the issue's deletion-masking repro).
This MR makes the error path never-cacheable at each writer, per the issue's accepted expectation: error responses carry Cache-Control: no-store so no intermediary retains them. Success-shape cache directives are !2296's half of the family (it explicitly defers error responses here); the redirect half is Closes #1133 there.
Research finding: the issue's suggested fix location was wrong
Work item #1131 (closed) says adding no-store to internal/transport/httperror.go "covers every endpoint at once". The code contradicts this (stated as Hypothesis, not a finding in the issue):
- Format packages own their transport surfaces and do not depend on
internal/transport(maven/problem.goandnpm/transport.gopackage docs;oci/errors.golikewise). - The observed poisoned
404(Maven read path, S01 JSON envelope) is written bymaven.writeS01JSON/maven.WriteProblem, not bytransport.WriteError.
The fix therefore lands in the three S01-envelope error writers, chosen with the operator (scope question on the issue): Maven + npm + transport, OCI excluded because its remote read 404 deliberately carries no directive (pinned by TestWriteRemoteReadError_SetsNoStoreOnA501Only, tracked in the #1084 family).
What changed
internal/transport/httperror.go—WriteErrorWithDetails(the single implementationWriteErrordelegates to) stampsCache-Control: no-store. Covers the management API, the GitLab API surface, auth, and the stdlib 404/405 rewrite.WriteJSONstays bare: it also serves success bodies, a distinction pinned inTestWriteJSON_ContentType.internal/format/maven/problem.go—WriteProblem(all RFC 9457 Maven errors) andwriteS01JSON(the read-path 404, the observed poison) stampno-store, via oneerrorCacheControlconst.maven/denial.go's now-redundant per-site set was removed; the directive ridesWriteProbleminstead.internal/format/npm/transport.go—WriteErrorstampsno-store, covering npm hosted errors and, redundantly-but-consistently, the remote proxy path's envelope (remote_read_errors.goalready set it).- OCI is untouched:
oci.WriteErrorandwriteRemoteReadErrorkeep their deliberate per-status stances (#1084 family).
Specs amended in the same MR: S01 (standard error format), S09 (management reads: private, max-age=0 + Vary: Authorization on success statuses, no-store on error statuses), S10 (read-path 404 shape + error envelope), S11 (Error Cases preamble), S14 (error mapping). S15 already stated the rule for npm proxy errors; no change needed there.
e2e.maven.consume.error-not-cacheable and e2e.npm.consume.error-not-cacheable were added to the catalogs.
Testing
- New/updated unit tests:
TestWriteError_FullEnvelope+TestWriteErrorWithDetails_CarriesDetails(transport),TestWriteProblem_IsNeverCacheable+TestWriteS01NotFound_IsNeverCacheable(maven),TestWriteError_S01EnvelopeShapeassertion (npm), and the pinnedWriteJSON-stays-bare distinction. All were red against the unmodified writers, then green. go testfull forinternal/transport,internal/format/maven,internal/format/npm, plusinternal/server,internal/managementapi,internal/gitlabapi,internal/auth— all pass.golangci-lint runclean on all touched packages, both with and without--build-tags=integration(no findings on touched lines).gofmtclean,gitlintclean,scripts/ci/check-comment-caps.shclean.- Conformance (run locally, both green):
scripts/conformance/maven_setup.sh— 31/31 PASS against the runnerv1.64.7, includingmaven.error.not-found-artifactandmaven.error.rfc-9457-problem-details, the touched surfaces.scripts/conformance/npm-e2e.sh— all steps passed (publish, install, view-404, unpublish, dist-tags).
- Pre-commit note (honesty): commits passed every hook except
go-fmtandgo-imports, which were skipped on the fix commit with a diagnosed root cause:golangci-lint2.13 (bumped 2026-09-02) embeds a formatter whose canonical output for a pre-existing struct-literal construct ininternal/format/npm/disttags_test.go(lines ~2255, not authored here) disagrees with standalonegofmt1.26.7 — the two hooks fight over that construct on main, and no content satisfies both. The committed file isgolangci-lint-clean (the CI-lint canonical form, matching main);gofmt -lflags only that pre-existing region. The config drift is worth a follow-up (the operator's other worktree hit the same fight). - Review (
review-branch, 116 reviewable LOC, solo): APPROVE. Two WARNINGs, none in a high-risk phase:!2296and!2304both add rows to the same e2e catalogs (docs/testing/e2e/maven.md/npm.md) and conflict against this branch while merging cleanly intomain— the later-merged MR rebases. The!2217-!2219stack conflicts only on gitleaks/.tool-versionsdivergence (also againstmain), not on shared content, and is not attributable to this MR. Clean overlaps checked:!2297,!2306,!2307,!2266.
Review follow-up
GitLab Duo review noted that npm.WriteJSON had no pinned-bare guard mirroring the transport package; addressed in commit 467f6264 (test(npm): pin WriteJSON bare on success responses), which adds the assert.Empty on the success path. AppSec review: no findings.
Post-Deploy Monitoring & Validation
This change only adds Cache-Control: no-store to error responses; no log line, metric, route, or payload shape changed.
- Validation window: 30 minutes after the deploy reaches staging, immediately after a deploy of a version containing this change.
- Log / metrics to watch: none specific — error response volume is unchanged. The signal is a response-header check, not a dashboard.
- Expected healthy signals: a
GETof a never-published Maven coordinate returns404withCache-Control: no-store(origin and through the edge); after publishing at that coordinate, the plain URL serves the artifact; the edge never answerscf-cache-status: HITon the error. - Failure signals / rollback: if the header is absent on a
404, the change did not roll out (check the running image tag). Rollback is a plain revert of this MR — the header is the only change and removing it restores prior behavior with no state to unwind.
Related to #1084 (OCI error-cache governance stays out of scope here) Closes #1131 (closed)