diff --git a/go.mod b/go.mod index 59feed98..d77d3de9 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ module miniflux.app/v2 require ( github.com/PuerkitoBio/goquery v1.9.1 github.com/abadojack/whatlanggo v1.0.1 + github.com/andybalholm/brotli v1.1.0 github.com/coreos/go-oidc/v3 v3.10.0 github.com/go-webauthn/webauthn v0.10.2 github.com/gorilla/mux v1.8.1 @@ -27,7 +28,6 @@ require ( ) require ( - github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/internal/http/response/builder.go b/internal/http/response/builder.go index caf48d0b..db89654a 100644 --- a/internal/http/response/builder.go +++ b/internal/http/response/builder.go @@ -12,6 +12,8 @@ import ( "net/http" "strings" "time" + + "github.com/andybalholm/brotli" ) const compressionThreshold = 1024 @@ -110,8 +112,15 @@ func (b *Builder) writeHeaders() { func (b *Builder) compress(data []byte) { if b.enableCompression && len(data) > compressionThreshold { acceptEncoding := b.r.Header.Get("Accept-Encoding") - switch { + case strings.Contains(acceptEncoding, "br"): + b.headers["Content-Encoding"] = "br" + b.writeHeaders() + + brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression) + defer brotliWriter.Close() + brotliWriter.Write(data) + return case strings.Contains(acceptEncoding, "gzip"): b.headers["Content-Encoding"] = "gzip" b.writeHeaders() diff --git a/internal/http/response/builder_test.go b/internal/http/response/builder_test.go index ccc29c8c..e5710b08 100644 --- a/internal/http/response/builder_test.go +++ b/internal/http/response/builder_test.go @@ -228,7 +228,7 @@ func TestBuildResponseWithCachingAndEtag(t *testing.T) { } } -func TestBuildResponseWithGzipCompression(t *testing.T) { +func TestBuildResponseWithBrotliCompression(t *testing.T) { body := strings.Repeat("a", compressionThreshold+1) r, err := http.NewRequest("GET", "/", nil) r.Header.Set("Accept-Encoding", "gzip, deflate, br") @@ -245,6 +245,30 @@ func TestBuildResponseWithGzipCompression(t *testing.T) { handler.ServeHTTP(w, r) resp := w.Result() + expected := "br" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithGzipCompression(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip, deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + expected := "gzip" actual := resp.Header.Get("Content-Encoding") if actual != expected { diff --git a/internal/reader/fetcher/response_handler.go b/internal/reader/fetcher/response_handler.go index 1aba5957..1de3b384 100644 --- a/internal/reader/fetcher/response_handler.go +++ b/internal/reader/fetcher/response_handler.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "os" + "strings" "miniflux.app/v2/internal/locale" ) @@ -73,15 +74,16 @@ func (r *ResponseHandler) Close() { } func (r *ResponseHandler) getReader(maxBodySize int64) io.ReadCloser { + contentEncoding := strings.ToLower(r.httpResponse.Header.Get("Content-Encoding")) slog.Debug("Request response", slog.String("effective_url", r.EffectiveURL()), - slog.Int64("content_length", r.httpResponse.ContentLength), - slog.String("content_encoding", r.httpResponse.Header.Get("Content-Encoding")), + slog.String("content_length", r.httpResponse.Header.Get("Content-Length")), + slog.String("content_encoding", contentEncoding), slog.String("content_type", r.httpResponse.Header.Get("Content-Type")), ) reader := r.httpResponse.Body - switch r.httpResponse.Header.Get("Content-Encoding") { + switch contentEncoding { case "br": reader = NewBrotliReadCloser(r.httpResponse.Body) case "gzip": diff --git a/internal/ui/feed_icon.go b/internal/ui/feed_icon.go index f83f3f4c..89d74903 100644 --- a/internal/ui/feed_icon.go +++ b/internal/ui/feed_icon.go @@ -29,7 +29,9 @@ func (h *handler) showIcon(w http.ResponseWriter, r *http.Request) { b.WithHeader("Content-Security-Policy", `default-src 'self'`) b.WithHeader("Content-Type", icon.MimeType) b.WithBody(icon.Content) - b.WithoutCompression() + if icon.MimeType != "image/svg+xml" { + b.WithoutCompression() + } b.Write() }) } diff --git a/internal/ui/static_app_icon.go b/internal/ui/static_app_icon.go index 668becd8..8a99fb62 100644 --- a/internal/ui/static_app_icon.go +++ b/internal/ui/static_app_icon.go @@ -31,12 +31,12 @@ func (h *handler) showAppIcon(w http.ResponseWriter, r *http.Request) { switch filepath.Ext(filename) { case ".png": + b.WithoutCompression() b.WithHeader("Content-Type", "image/png") case ".svg": b.WithHeader("Content-Type", "image/svg+xml") } - b.WithoutCompression() b.WithBody(blob) b.Write() })