feat(npm): tarball download conditional GET + validators (S11 Step 9, 6/6)
📦 What
S11 Step 9 (tarball download) is a six-MR stack, in dependency order (each targeted the one above; GitLab auto-retargeted to main as they merged):
- !535 (merged) - two-id Resolution for npm read handlers.
- !543 (merged) -
NpmVersionByPackageAndVersiondatastore finder (read-side prerequisite). - !536 (merged) - tarball download handler: serve + Cache-Control.
- !562 (merged) -
last_downloaded_atbump on a served download. - !563 (merged) - perf: resolve the version by an indexed point lookup.
- !537 (merged) - conditional GET + strong validators. <- this MR
The rest of the stack (!535 (merged), !543 (merged), !536 (merged), !562 (merged), !563 (merged)) has merged; this MR now targets main directly (rebased onto it).
Layers the strong-validator cache headers and the conditional-GET 304 short-circuit onto the handler from slice 2:
ETag: "sha256-<hex>"(overnpm_files.blob_sha256) andLast-Modified(npm_files.created_at) on every response.If-None-Match(including comma-separated lists and the weakW/form, per RFC 9110 §13.1.2) /If-Modified-Sincereturn304 Not Modifiedbefore anyBlobStore.OpenBlobcall: a conditional-GET validation is not a download, so it touches neither storage norlast_downloaded_at. The 304 short-circuit is scoped toGET/HEAD.
Amends the S11 spec's tarball-download API contract to add the Response 304 / Headers / Conditional GET rows (the route previously prescribed no cache headers).
✅ Spec coverage
| Behaviour | Test |
|---|---|
ETag "sha256-"+hex(blob_sha256) on 200 |
TestDownloadHandler_GET_ServesTarballWithCacheHeaders |
matching If-None-Match -> 304, empty body, ZERO OpenBlob |
TestDownloadHandler_IfNoneMatch_Returns304WithoutStorageIO |
non-matching If-None-Match -> 200 |
TestDownloadHandler_IfNoneMatch_NonMatchServesBody |
If-Modified-Since >= created_at -> 304, older -> 200 |
TestDownloadHandler_IfModifiedSince |
malformed If-Modified-Since -> safe 200 (no 304/500) |
TestDownloadHandler_IfModifiedSince_Malformed |
304 does not bump last_downloaded_at |
TestDownloadHandler_304_DoesNotBump |
comma-list / weak W/ If-None-Match -> 304 |
TestDownloadHandler_IfNoneMatch_ListMatchReturns304, TestDownloadHandler_IfNoneMatch_WeakValidatorReturns304 |
non-GET/HEAD never 304s (method guard) |
TestConditionalMatch_NonGetHeadMethodNeverMatches |
⚠️ Known gap
Same as slice 2: the handler is still unmounted (handler.go serves the 501 placeholder). Unassigned plan-level wiring gap, to be closed via a plan amendment before Step 23 conformance.
🧪 Testing
go test ./internal/format/npm/...- passgolangci-lint run ./internal/format/npm/...- 0 issuesgo vet -tags integration ./internal/format/npm/... ./internal/datastore/... ./cmd/artifact-registry/...- clean
Related to #127 (closed)