diff --git a/README.md b/README.md index eea7171..08bb4a2 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ should not be compared across sources. Face detection does not perform identity recognition. Blurred, obscured, or highly stylized faces may use the saliency fallback. -Requests are limited to 10 MiB and decoded images to 20 megapixels. Separate +Requests are limited to 32 MiB and decoded images to 40 megapixels. Separate upload and analysis admission limits bound buffered-body and decoded-image memory without allowing slow uploads to reserve inference capacity. Both models are loaded once at startup and their inference sessions are reused safely diff --git a/cmd/autogravity/main.go b/cmd/autogravity/main.go index 743b01e..65d95b7 100644 --- a/cmd/autogravity/main.go +++ b/cmd/autogravity/main.go @@ -28,7 +28,7 @@ import ( ) const ( - maxRequestBytes = 10 << 20 // 10 MiB, including multipart overhead. + maxRequestBytes = 32 << 20 // 32 MiB, including multipart overhead. maxConcurrentUploads = 4 // Bounds buffered bodies without reserving inference. defaultIntraOpThreads = 1 // Avoid N requests multiplying ONNX worker threads. defaultFaceModelPath = "models/face_detection_yunet_2023mar.onnx" @@ -290,7 +290,7 @@ func (app *application) handleAnalyze(w http.ResponseWriter, r *http.Request) { outcome = "invalid_request" var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { - writeError(w, http.StatusRequestEntityTooLarge, "image exceeds the 10 MiB request limit") + writeError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("image exceeds the %d MiB request limit", maxRequestBytes>>20)) return } writeError(w, http.StatusBadRequest, err.Error()) diff --git a/cmd/autogravity/main_test.go b/cmd/autogravity/main_test.go index 68242a1..0079646 100644 --- a/cmd/autogravity/main_test.go +++ b/cmd/autogravity/main_test.go @@ -18,6 +18,7 @@ import ( "time" "autogravity/internal/facedetection" + "autogravity/internal/imageutil" "autogravity/internal/saliency" ) @@ -302,6 +303,7 @@ func TestHandleAnalyzeErrors(t *testing.T) { {name: "unsupported format", method: http.MethodPost, contentType: "application/octet-stream", body: []byte("not an image"), wantStatus: http.StatusUnsupportedMediaType}, {name: "empty body", method: http.MethodPost, contentType: "image/png", wantStatus: http.StatusBadRequest}, {name: "request too large", method: http.MethodPost, contentType: "image/png", body: make([]byte, maxRequestBytes+1), wantStatus: http.StatusRequestEntityTooLarge}, + {name: "11 MiB body is within limit", method: http.MethodPost, contentType: "image/png", body: make([]byte, 11<<20), wantStatus: http.StatusUnsupportedMediaType}, } for _, tt := range tests { @@ -319,6 +321,21 @@ func TestHandleAnalyzeErrors(t *testing.T) { } } +func TestProductionPhotographFitsLimits(t *testing.T) { + const ( + productionPNGBytes = 28_026_017 + productionWidth = 6000 + productionHeight = 4000 + ) + if maxRequestBytes < productionPNGBytes { + t.Fatalf("maxRequestBytes = %d, production photograph is %d bytes", maxRequestBytes, productionPNGBytes) + } + pixels := int64(productionWidth) * int64(productionHeight) + if pixels > imageutil.MaxPixels { + t.Fatalf("MaxPixels = %d, production photograph is %d pixels", imageutil.MaxPixels, pixels) + } +} + func TestHandleAnalyzeBoundsConcurrentWork(t *testing.T) { analyzer := &fakeAnalyzer{delay: 10 * time.Millisecond} const concurrency = 2 diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index c3f7fde..55e6043 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -227,7 +227,7 @@ function DocsPage() {

- Requests are limited to 10 MiB and decoded images to 20 megapixels. + Requests are limited to 32 MiB and decoded images to 40 megapixels. Separate upload and analysis admission limits bound buffered-body and decoded-image memory without allowing slow uploads to reserve inference capacity. Both models are loaded once at startup and their diff --git a/internal/imageutil/image.go b/internal/imageutil/image.go index 1f54ced..a2abcd8 100644 --- a/internal/imageutil/image.go +++ b/internal/imageutil/image.go @@ -15,7 +15,10 @@ import ( _ "golang.org/x/image/webp" ) -const MaxPixels = 20_000_000 +// MaxPixels is the decoded width*height ceiling. 40 megapixels covers common +// 24 MP camera photographs (for example 6000x4000) while still rejecting +// decompression bombs before pixels are allocated. +const MaxPixels = 40_000_000 var ( ErrUnsupportedFormat = errors.New("unsupported image format") diff --git a/internal/imageutil/image_test.go b/internal/imageutil/image_test.go index 827901f..dc0ad9c 100644 --- a/internal/imageutil/image_test.go +++ b/internal/imageutil/image_test.go @@ -152,12 +152,22 @@ func TestDecodeRejectsUnknownFormat(t *testing.T) { } func TestDecodeRejectsExcessivePixelCount(t *testing.T) { - _, err := Decode(oversizedPNGHeader(5_000, 5_000)) + _, err := Decode(oversizedPNGHeader(8_000, 8_000)) if !errors.Is(err, ErrImageTooLarge) { t.Fatalf("Decode() error = %v, want ErrImageTooLarge", err) } } +func TestDecodeAccepts24MegapixelCameraPhoto(t *testing.T) { + _, err := Decode(oversizedPNGHeader(6000, 4000)) + if errors.Is(err, ErrImageTooLarge) { + t.Fatal("rejected 6000x4000 photograph that production must accept") + } + if err == nil { + t.Fatal("header-only fixture unexpectedly decoded") + } +} + func TestPrepareProducesNormalizedNCHW(t *testing.T) { img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.SetRGBA(0, 0, color.RGBA{R: 255, G: 128, B: 0, A: 255})