feat(response): almost standard-compliant Accept-Encoding parser

This commit is contained in:
gudvinr
2026-06-13 19:05:21 +03:00
committed by fguillot
parent 51f2e0d819
commit 7769fa06ef
2 changed files with 210 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package response
import (
"slices"
"strconv"
"strings"
)
type acceptEncodingParser struct {
// accepted contains all encoding that particular parser instance advertises.
accepted []string
}
// AcceptEncoding creates parser instance for "Accept-Encoding" header values.
// It accepts list of encodings recognized by user of this parser instance.
func AcceptEncoding(accepted ...string) *acceptEncodingParser {
return &acceptEncodingParser{accepted: accepted}
}
// Parse 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 (p *acceptEncodingParser) Parse(acceptEncoding string) 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:], ";")
qstr = strings.TrimSpace(qstr)
qstr = strings.TrimPrefix(qstr, "q=")
q, err := strconv.ParseFloat(qstr, 64)
if err != nil {
continue // Ignore weird float values.
}
enc = strings.TrimSpace(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.
}
}
if !slices.Contains(p.accepted, enc) {
continue // Skipping unsupported.
}
accepted = enc
break
}
return accepted
}
+138
View File
@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package response
import (
"testing"
)
func TestAcceptEncoding(t *testing.T) {
t.Parallel()
acceptable := []string{
"br", "gzip", "deflate",
}
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",
},
{
name: "wrong spaces placing around q value",
acceptEncoding: "gzip;q= 0.5, deflate;q=0.8",
want: "deflate",
},
{
name: "correct spaces placing around q value",
acceptEncoding: "gzip ; q=0.5, deflate;q=0.8",
want: "gzip",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Instantiate parser for each test to make sure it doesn't return cached values.
parser := AcceptEncoding(acceptable...)
got := parser.Parse(test.acceptEncoding)
if got != test.want {
t.Errorf("Parse(%q) = %q, want %q", test.acceptEncoding, got, test.want)
}
})
}
}
func BenchmarkAcceptEncoding(b *testing.B) {
encoding := "identity;q=0,gzip,whatever"
expected := "gzip"
parser := AcceptEncoding("br", "gzip", "deflate")
for b.Loop() {
got := parser.Parse(encoding)
if got != expected {
b.Errorf("Parse(%q) = %q, want %q", encoding, got, expected)
}
}
}