SCRUM-226 — Patch: manifest endpoint with ETag / 304¶
Plan ref: PAT-B5 (docs/11-admin-plane-plan.md). Stacked on SCRUM-225.
Behaviour — GET /patch/v1/{channel}/manifest (HEAD too)¶
| Case | Response |
|---|---|
| channel ∉ dev/staging/live | 404 not_found "no such channel" |
dev / staging without a valid staff token |
401 with the verifier's code (re-checked here; the player gateway also gates them) |
| staff token with no role | 403 insufficient_role |
| channel not loaded yet | 503 not_ready "manifest for |
If-None-Match matches (*, a list, weak comparison ignoring W/) |
304, empty body |
| otherwise | 200, the canonical manifest bytes verbatim |
200 and 304 both carry ETag: "<manifest_sha256>", Cache-Control: no-cache (the
client must revalidate; a silently stale manifest is a client on the wrong build)
and X-Min-Client-Version. The read path is one atomic load and a map lookup: no
locks, no JSON encoding. Metric patch_manifest_requests_total{channel,result="200|304"};
auth rejections reuse patch_token_rejected_total.
How to verify¶
cd services/patch
export PATCH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run 'Manifest|IfNoneMatch' ./internal/api/ ./internal/server/
| Test | Proves |
|---|---|
TestIfNoneMatch |
exact, list, spaces, W/ either side, *, no match, malformed members ignored, empty header |
TestManifestLiveServesBytesAndValidators / …Revalidation / …HeadServesNoBody |
body byte-for-byte, three headers; 304 with headers and no body; HEAD no body |
TestManifestErrors |
404 unknown channel, 503 not loaded |
TestManifestRestrictedChannelAuth |
dev: no token 401, player-issuer token 401, roleless staff 403, viewer 200 |
TestManifestRouteServesAndCounts |
the metric counts 200 and 304 separately |
TestManifestServesBootstrappedLive (DB) |
the real live manifest; ETag = "+manifest_sha256+"; 304 on revalidation |
Live check done during review (real binary, test DB):
GET /patch/v1/live/manifest -> 200, Etag "476ad8d1…07f4" (= channel_head's manifest_sha256),
Cache-Control: no-cache, X-Min-Client-Version: 0.0.0,
{"channel":"live","config":{},"format":1,"min_client_version":"0.0.0","packs":[],"release_id":3}
+ If-None-Match: <that etag> -> 304, 0 body bytes
GET /patch/v1/staging/manifest (no token) -> 401 missing_token
patch_manifest_requests_total{channel="live",result="200"} 3, {…,result="304"} 1
Results at time of writing¶
go vet,go test -race(6 packages) against Postgres 16: pass; live check above.
How it was built¶
DeepSeek run scoped (Landlock) to services/patch (161 s, ~29k output tokens).
Claude review: handler and If-None-Match parser read; curl check. No changes needed.