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 ( | |||
| 22 | 22 | const ( | |
| 23 | 23 | // MaxManifestSize limits manifest uploads to 10MB. | |
| 24 | 24 | 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 | |
| 27 | 29 | ) | |
| 28 | 30 | ||
| 29 | 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 | 375 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid)) | |
| 374 | 376 | w.Header().Set("Range", "0-0") | |
| 377 | + | w.Header().Set("Docker-Upload-UUID", uuid) | |
| 375 | 378 | w.WriteHeader(http.StatusAccepted) | |
| 376 | 379 | } | |
| 377 | 380 | ||
| @@ -440,6 +443,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 440 | 443 | // For PATCH requests, respond with 202 Accepted | |
| 441 | 444 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid)) | |
| 442 | 445 | w.Header().Set("Range", fmt.Sprintf("0-%d", length-1)) | |
| 446 | + | w.Header().Set("Docker-Upload-UUID", uuid) | |
| 443 | 447 | w.WriteHeader(http.StatusAccepted) | |
| 444 | 448 | } | |
| 445 | 449 | ||
| @@ -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 * 1024 | 24 | MaxManifestSize = 10 * 1024 * 1024 | ||
| 25 | - | // MaxBlobChunkSize limits individual blob chunks to 100MB. | 25 | + | // MaxBlobChunkSize limits individual blob chunks to 95MB. |
| 26 | - | MaxBlobChunkSize = 100 * 1024 * 1024 | 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 | |||
| 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 Accepted | 443 | // 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) { | |||
| 409 | 409 | ||
| 410 | 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 | + | } | |
| @@ -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 | + | } | |||