Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions internal/http/response/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,26 @@ func (b *Builder) writeHeaders() {
func (b *Builder) compress(data []byte) {
if b.enableCompression && len(data) > compressionThreshold {
b.headers.Set("Vary", "Accept-Encoding")
acceptEncoding := b.r.Header.Get("Accept-Encoding")
switch {
case strings.Contains(acceptEncoding, "br"):

encoding := AcceptEncoding(b.r.Header.Get("Accept-Encoding"))
switch encoding {
case "br":
b.headers.Set("Content-Encoding", "br")
b.writeHeaders()

brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
brotliWriter.Write(data)
brotliWriter.Close()
return
case strings.Contains(acceptEncoding, "gzip"):
case "gzip":
b.headers.Set("Content-Encoding", "gzip")
b.writeHeaders()

gzipWriter := gzip.NewWriter(b.w)
gzipWriter.Write(data)
gzipWriter.Close()
return
case strings.Contains(acceptEncoding, "deflate"):
case "deflate":
b.headers.Set("Content-Encoding", "deflate")
b.writeHeaders()

Expand Down
2 changes: 1 addition & 1 deletion internal/http/response/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func TestIfNoneMatch(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")
r.Header.Set("Accept-Encoding", "br, gzip, deflate")
if err != nil {
t.Fatal(err)
}
Expand Down
75 changes: 75 additions & 0 deletions internal/http/response/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package response

import (
"slices"
"strconv"
"strings"
"sync"
)

// encodingCache stores mapping between raw Accept-Encoding value
// and chosen encoding from this value.
var encodingCache sync.Map

// AcceptEncoding parses input string according to [HTTP Semantics]
// and returns first encoding that can be understood by us.
//
// Currently this function ignores set weights other than q=0.
// Encodings with q=0 will not be considered.
//
// If string is empty or no encoding was accepted function returns "identity".
//
// For "identity;q=0" and "*;q=0" function returns an empty string. In that case,
// if no other encoding was accepted, 406 Not Acceptable should be returned.
//
// [HTTP Semantics]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding.
func AcceptEncoding(acceptEncoding string) string {
acceptEncoding = strings.TrimSpace(acceptEncoding)
if v, ok := encodingCache.Load(acceptEncoding); ok {
return v.(string)
}

accepted := "identity"

for enc := range strings.SplitSeq(acceptEncoding, ",") {
enc = strings.TrimSpace(enc)

if qi := strings.IndexByte(enc, ';'); qi > -1 {
qstr := strings.TrimPrefix(enc[qi:], ";q=")

q, err := strconv.ParseFloat(qstr, 64)
if err != nil {
continue // Ignore weird float values.
}

enc = enc[:qi]

if q == 0 && slices.Contains([]string{"identity", "*"}, enc) {
accepted = "" // Explicitly disabled, so can't be used as fallback.
continue
}

if q == 0 {
continue // Skipping unwanted.
}
}

// List should be in sync with [Builder.Write].
if !slices.Contains([]string{"br", "gzip", "deflate"}, enc) {
continue // Skipping unsupported.
}

accepted = strings.ToLower(enc)
break
}

// Store selection as it won't change for given header value.
if v, ok := encodingCache.LoadOrStore(acceptEncoding, accepted); ok {
return v.(string)
}

return accepted
}
125 changes: 125 additions & 0 deletions internal/http/response/encoding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package response_test

import (
"crypto/rand"
"testing"

"miniflux.app/v2/internal/http/response"
)

func TestAcceptEncoding(t *testing.T) {
t.Parallel()

tests := []struct {
name string
acceptEncoding string
want string
}{
{
name: "Empty input",
acceptEncoding: "",
want: "identity",
},
{
name: "q=0 and identity",
acceptEncoding: "identity;q=0",
want: "",
},
{
name: "q=0 and *",
acceptEncoding: "*;q=0",
want: "",
},
{
name: "gzip",
acceptEncoding: "gzip",
want: "gzip",
},
{
name: "gzip and br",
acceptEncoding: "gzip,br",
want: "gzip",
},
{
name: "br and gzip",
acceptEncoding: "br,gzip,deflate",
want: "br",
},
{
name: "unsupported encoding",
acceptEncoding: "unknown",
want: "identity",
},
{
name: "empty encoding",
acceptEncoding: ",",
want: "identity",
},
{
name: "multiple encodings and q=0",
acceptEncoding: "gzip;q=0,br;q=0",
want: "identity",
},
{
// We want br here but weights are not supported.
name: "multiple encodings and q values",
acceptEncoding: "gzip;q=0.5,br;q=0.8",
want: "gzip",
},
{
name: "multiple encodings and wildcard",
acceptEncoding: "*;q=0,gzip,br",
want: "gzip",
},
{
name: "multiple encodings and wildcard and q=0",
acceptEncoding: "*;q=0,gzip,br;q=0",
want: "gzip",
},
{
// We want br here but weights are not supported.
name: "multiple encodings and wildcard and q values",
acceptEncoding: "*;q=0.5,gzip;q=0.8,br",
want: "gzip",
},
{
name: "multiple encodings and wildcard and q values and q=0",
acceptEncoding: "*;q=0.5,gzip;q=0.8,br;q=0",
want: "gzip",
},
{
name: "invalid q value",
acceptEncoding: "gzip;q=abc,deflate",
want: "deflate",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

got := response.AcceptEncoding(test.acceptEncoding)
if got != test.want {
t.Errorf("AcceptEncoding() = %q, want %q", got, test.want)
}
})
}
}

func TestAcceptEncodingCache(t *testing.T) {
encoding := "identity;q=0,gzip," + rand.Text() // rand for avoid clashes with other test cases
expected := "gzip"

got := response.AcceptEncoding(encoding)
if got != expected {
t.Errorf("AcceptEncoding() = %q, want %q", got, expected)
}

got = response.AcceptEncoding(encoding)
if got != expected {
t.Errorf("AcceptEncoding() = %q, want %q", got, expected)
}
}