From c585d7873e9b4bfd9f6efd30f9ce08aed8a0d92b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 21:33:02 +0000 Subject: Improve image proxy: streaming, size limits, Content-Type validation Rewrites the image proxy handler to address several issues: - Stream responses with io.Copy instead of buffering entire image in memory - Add 25MB size limit via io.LimitReader to prevent memory exhaustion - Close resp.Body (was previously leaked on every request) - Validate Content-Type is an image, rejecting HTML/JS/etc - Forward Content-Type and Content-Length from upstream - Use http.NewRequestWithContext to propagate client cancellation - Check upstream status codes, returning 502 for non-2xx - Fix ETag: use proper quoted format, remove bogus Etag request header check - Increase timeout from 5s to 30s for slow image servers - Use proper HTTP status codes (400 for bad input, 502 for upstream errors) - Add Cache-Control max-age directive alongside Expires header Tests: comprehensive coverage for Content-Type filtering, upstream errors, streaming, ETag validation, User-Agent forwarding, and Content-Length. Benchmarks: cache hit path and streaming at 1KB/64KB/1MB/5MB sizes. https://claude.ai/code/session_01CZcDDVmF6wNs2YjdhvCppy --- web/web_bench_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'web/web_bench_test.go') diff --git a/web/web_bench_test.go b/web/web_bench_test.go index 7897fc7..068fd55 100644 --- a/web/web_bench_test.go +++ b/web/web_bench_test.go @@ -1,8 +1,11 @@ package web import ( + "encoding/base64" + "fmt" "net/http" "net/http/httptest" + "net/url" "strings" "testing" @@ -90,3 +93,48 @@ func BenchmarkFullMiddlewareStack(b *testing.B) { handler.ServeHTTP(rr, req) } } + +// --- Image proxy benchmarks --- + +func BenchmarkImageProxyCacheHit(b *testing.B) { + encoded := base64.URLEncoding.EncodeToString([]byte("https://example.com/image.jpg")) + etag := `"` + encoded + `"` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/"+encoded, nil) + req.Header.Set("If-None-Match", etag) + rr := httptest.NewRecorder() + imageProxyHandler(rr, req) + } +} + +func benchmarkImageProxySize(b *testing.B, size int) { + data := make([]byte, size) + for i := range data { + data[i] = byte(i % 256) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("Content-Length", fmt.Sprintf("%d", size)) + w.Write(data) + })) + defer ts.Close() + + encoded := base64.URLEncoding.EncodeToString([]byte(ts.URL + "/img.jpg")) + + b.ResetTimer() + b.SetBytes(int64(size)) + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/"+encoded, nil) + req.URL = &url.URL{Path: encoded} + rr := httptest.NewRecorder() + imageProxyHandler(rr, req) + } +} + +func BenchmarkImageProxy_1KB(b *testing.B) { benchmarkImageProxySize(b, 1<<10) } +func BenchmarkImageProxy_64KB(b *testing.B) { benchmarkImageProxySize(b, 64<<10) } +func BenchmarkImageProxy_1MB(b *testing.B) { benchmarkImageProxySize(b, 1<<20) } +func BenchmarkImageProxy_5MB(b *testing.B) { benchmarkImageProxySize(b, 5<<20) } -- cgit v1.2.3