SCRUM-227 — Patch: content-addressed blob delivery¶
Plan ref: PAT-A2′ (docs/11-admin-plane-plan.md, decision D9: Go http.ServeContent
instead of an nginx sidecar). Stacked on SCRUM-226.
Behaviour — GET /patch/v1/blob/{sha256} (HEAD too), public¶
| Case | Response |
|---|---|
{sha256} not ^[0-9a-f]{64}$ |
400 validation_failed, checked before any filesystem access (the regex is the path-traversal guard) |
| unknown hash, or a non-regular file at the path | 404 not_found |
| found | streamed by http.ServeContent: Range → 206, unsatisfiable → 416, If-None-Match → 304 |
Headers: Content-Type: application/octet-stream,
Cache-Control: public, max-age=31536000, immutable, ETag: "<sha256>".
Path: <PATCH_BLOB_ROOT>/blobs/<sha[0:2]>/<sha[2:4]>/<sha> — exactly where
Config's blob store writes (verified end to end below). Patch does not hash on
every request; the client verifies what it downloads. Metric
patch_blob_requests_total{result="200|206|304|404|400"}.
Routing note: GET /patch/v1/blob/{sha256} cannot sit on the same ServeMux as
GET /patch/v1/{channel}/manifest — both match /patch/v1/blob/manifest, neither is
more specific, and ServeMux panics at registration. The blob prefix is dispatched
first (validating the hash before the mux can clean or redirect the path);
TestBlobAndManifestRoutesCoexist guards it.
How to verify¶
cd services/patch
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run Blob ./internal/server/
| Test | Proves |
|---|---|
TestBlobServesFullBodyWithHeaders |
200, body, headers, Content-Length |
TestBlobRangeRequests |
0-1023 → 206 + Content-Range; 100- tail; unsatisfiable → 416 |
TestBlobRevalidationReturns304, TestBlobHEADCarriesHeadersWithoutBody |
as named |
TestBlobNotFound |
unknown hash; a directory at the path |
TestBlobRejectsInvalidSHA |
literal and URL-encoded ../ traversal, 63 chars, uppercase, trailing segment → 400; a sentinel file above the root never appears in any body |
TestBlobMetricsCountPerResult, …RouteAbsentWithoutRoot, …AndManifestRoutesCoexist |
as named |
TestBlobStreamsLargeFileWithBoundedAllocation |
32 MiB served with < 8 MiB allocation |
Cross-service check (done during review)¶
Config's real blob.Store.Put wrote a 1.1 MB blob; the Patch binary pointed at the
same root served it:
full download -> 200, 1100000 bytes, sha256 of the download = the sha Config returned
Range: bytes=0-1023 -> 206, Content-Range: bytes 0-1023/1100000, immutable cache header, ETag = sha
..%2F..%2Fetc%2Fpasswd -> 400 validation_failed
Results at time of writing¶
go vet,go test -race(6 packages): pass; cross-service check as above.
How it was built¶
DeepSeek run scoped (Landlock) to services/patch (259 s, ~49k output tokens). It
found the ServeMux conflict and routed around it. Claude review: layout parity
checked against Config's code and on disk. No changes needed.