detached ec03e2f

fix(registry): reduce max chunk to 95MB and add Docker-Upload-UUID header

Leave headroom below Cloudflare's 100MB per-request limit.
Add Docker-Upload-UUID header per OCI spec so clients can
properly track chunked upload sessions.
commit
ec03e2f3193a52c91bceeb070c7326003b0eb00f
author
brice · · 2026-02-06 13:01 UTC
parent
893672d
signature
unsigned
2 files · +43 -2
M internal/adapters/in/http/registry/handler.go +6 -2
@@ -22,8 +22,10 @@ import (
2222 const (
2323 // MaxManifestSize limits manifest uploads to 10MB.
2424 MaxManifestSize = 10 * 1024 * 1024
25- // MaxBlobChunkSize limits individual blob chunks to 100MB.
26- MaxBlobChunkSize = 100 * 1024 * 1024
25+ // MaxBlobChunkSize limits individual blob chunks to 95MB.
26+ // Kept below 100MB to leave headroom for HTTP overhead when behind
27+ // proxies like Cloudflare that enforce a 100MB per-request limit.
28+ MaxBlobChunkSize = 95 * 1024 * 1024
2729 )
2830
2931 // Handler implements the HTTP handler for Docker Registry API v2.
@@ -372,6 +374,7 @@ func (h *Handler) handleStartBlobUpload(w http.ResponseWriter, r *http.Request)
372374
373375 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))
374376 w.Header().Set("Range", "0-0")
377+ w.Header().Set("Docker-Upload-UUID", uuid)
375378 w.WriteHeader(http.StatusAccepted)
376379 }
377380
@@ -440,6 +443,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
440443 // For PATCH requests, respond with 202 Accepted
441444 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))
442445 w.Header().Set("Range", fmt.Sprintf("0-%d", length-1))
446+ w.Header().Set("Docker-Upload-UUID", uuid)
443447 w.WriteHeader(http.StatusAccepted)
444448 }
445449
@@ -22,8 +22,10 @@ import (
22 const (22 const (
23 // MaxManifestSize limits manifest uploads to 10MB.23 // MaxManifestSize limits manifest uploads to 10MB.
24 MaxManifestSize = 10 * 1024 * 102424 MaxManifestSize = 10 * 1024 * 1024
25- // MaxBlobChunkSize limits individual blob chunks to 100MB.25+ // MaxBlobChunkSize limits individual blob chunks to 95MB.
26- MaxBlobChunkSize = 100 * 1024 * 102426+ // Kept below 100MB to leave headroom for HTTP overhead when behind
27+ // proxies like Cloudflare that enforce a 100MB per-request limit.
28+ MaxBlobChunkSize = 95 * 1024 * 1024
27 )29 )
28 30
29 // Handler implements the HTTP handler for Docker Registry API v2.31 // Handler implements the HTTP handler for Docker Registry API v2.
@@ -372,6 +374,7 @@ func (h *Handler) handleStartBlobUpload(w http.ResponseWriter, r *http.Request)
372 374
373 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))375 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))
374 w.Header().Set("Range", "0-0")376 w.Header().Set("Range", "0-0")
377+ w.Header().Set("Docker-Upload-UUID", uuid)
375 w.WriteHeader(http.StatusAccepted)378 w.WriteHeader(http.StatusAccepted)
376 }379 }
377 380
@@ -440,6 +443,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
440 // For PATCH requests, respond with 202 Accepted443 // For PATCH requests, respond with 202 Accepted
441 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))444 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid))
442 w.Header().Set("Range", fmt.Sprintf("0-%d", length-1))445 w.Header().Set("Range", fmt.Sprintf("0-%d", length-1))
446+ w.Header().Set("Docker-Upload-UUID", uuid)
443 w.WriteHeader(http.StatusAccepted)447 w.WriteHeader(http.StatusAccepted)
444 }448 }
445 449
M internal/adapters/in/http/registry/handler_test.go +37 -0
@@ -409,3 +409,40 @@ func TestHandler_RegisterRoutes(t *testing.T) {
409409
410410 assert.Equal(t, http.StatusOK, rec.Code)
411411 }
412+
413+func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) {
414+ registrySvc := inmocks.NewMockRegistryService(t)
415+ handler := NewHandler(registrySvc, testLogger())
416+
417+ registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
418+
419+ req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
420+ rec := httptest.NewRecorder()
421+
422+ handler.ServeHTTP(rec, req)
423+
424+ assert.Equal(t, http.StatusAccepted, rec.Code)
425+ assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
426+}
427+
428+func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) {
429+ registrySvc := inmocks.NewMockRegistryService(t)
430+ handler := NewHandler(registrySvc, testLogger())
431+
432+ chunkData := []byte("chunk content")
433+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil)
434+
435+ req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
436+ rec := httptest.NewRecorder()
437+
438+ handler.ServeHTTP(rec, req)
439+
440+ assert.Equal(t, http.StatusAccepted, rec.Code)
441+ assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
442+}
443+
444+func TestMaxBlobChunkSize_Under100MB(t *testing.T) {
445+ // Verify the constant leaves headroom below Cloudflare's 100MB limit
446+ assert.Less(t, int64(MaxBlobChunkSize), int64(100*1024*1024))
447+ assert.Greater(t, int64(MaxBlobChunkSize), int64(90*1024*1024))
448+}
@@ -409,3 +409,40 @@ func TestHandler_RegisterRoutes(t *testing.T) {
409 409
410 assert.Equal(t, http.StatusOK, rec.Code)410 assert.Equal(t, http.StatusOK, rec.Code)
411 }411 }
412+
413+func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) {
414+ registrySvc := inmocks.NewMockRegistryService(t)
415+ handler := NewHandler(registrySvc, testLogger())
416+
417+ registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
418+
419+ req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
420+ rec := httptest.NewRecorder()
421+
422+ handler.ServeHTTP(rec, req)
423+
424+ assert.Equal(t, http.StatusAccepted, rec.Code)
425+ assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
426+}
427+
428+func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) {
429+ registrySvc := inmocks.NewMockRegistryService(t)
430+ handler := NewHandler(registrySvc, testLogger())
431+
432+ chunkData := []byte("chunk content")
433+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil)
434+
435+ req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
436+ rec := httptest.NewRecorder()
437+
438+ handler.ServeHTTP(rec, req)
439+
440+ assert.Equal(t, http.StatusAccepted, rec.Code)
441+ assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
442+}
443+
444+func TestMaxBlobChunkSize_Under100MB(t *testing.T) {
445+ // Verify the constant leaves headroom below Cloudflare's 100MB limit
446+ assert.Less(t, int64(MaxBlobChunkSize), int64(100*1024*1024))
447+ assert.Greater(t, int64(MaxBlobChunkSize), int64(90*1024*1024))
448+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close