Skip to content

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.