mirror of https://github.com/gofiber/fiber.git
🐛fix: update getOffer to consider quality and specificity (#2486)
* feat: getOffer consider q value and specificity * fix: ignore q=0 * fix: float * test: client-prefered order and q=0 not acceptable * fix: always use my insertion sort. * fix: sort.SliceStable if > 20 * fix: zero allocations * perf: optimize the sort * chore: fix lint issue * fix: consider order * chore: fix test func name * chore: fix helper test func name * chore: revert fix * perf: use fasthttp.ParseUfloat * test: GetOffer and SortAcceptedTypes * chore: remote nil check * test: sortAcceptedTypes * fix: use utils.UnsafeBytes * docs: update docs for fiber PR #2486 * docs: update docs for fiber PR #2486 * test: add test from docs * fix: yamlpull/2502/head
parent
d91ea9e01e
commit
0f5ffed3cc
|
@ -45,6 +45,10 @@ func Test_Ctx_Accepts(t *testing.T) {
|
|||
utils.AssertEqual(t, "", c.Accepts())
|
||||
utils.AssertEqual(t, ".xml", c.Accepts(".xml"))
|
||||
utils.AssertEqual(t, "", c.Accepts(".john"))
|
||||
utils.AssertEqual(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type")
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, "application/json, text/plain, */*;q=0")
|
||||
utils.AssertEqual(t, "", c.Accepts("html"), "must treat */*;q=0 as not acceptable")
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, "text/*, application/json")
|
||||
utils.AssertEqual(t, "html", c.Accepts("html"))
|
||||
|
|
|
@ -24,19 +24,31 @@ func (c *Ctx) AcceptsLanguages(offers ...string) string
|
|||
```
|
||||
|
||||
```go title="Example"
|
||||
// Accept: text/*, application/json
|
||||
// Accept: text/html, application/json; q=0.8, text/plain; q=0.5; charset="utf-8"
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Accepts("html") // "html"
|
||||
c.Accepts("text/html") // "text/html"
|
||||
c.Accepts("json", "text") // "json"
|
||||
c.Accepts("application/json") // "application/json"
|
||||
c.Accepts("text/plain", "application/json") // "application/json", due to quality
|
||||
c.Accepts("image/png") // ""
|
||||
c.Accepts("png") // ""
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
```go title="Example 2"
|
||||
// Accept: text/html, text/*, application/json, */*; q=0
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Accepts("text/plain", "application/json") // "application/json", due to specificity
|
||||
c.Accepts("application/json", "text/html") // "text/html", due to first match
|
||||
c.Accepts("image/png") // "", due to */* without q factor 0 is Not Acceptable
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
Fiber provides similar functions for the other accept headers.
|
||||
|
||||
```go
|
||||
|
|
119
helpers.go
119
helpers.go
|
@ -25,6 +25,16 @@ import (
|
|||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// acceptType is a struct that holds the parsed value of an Accept header
|
||||
// along with quality, specificity, and order.
|
||||
// used for sorting accept headers.
|
||||
type acceptedType struct {
|
||||
spec string
|
||||
quality float64
|
||||
specificity int
|
||||
order int
|
||||
}
|
||||
|
||||
// getTLSConfig returns a net listener's tls config
|
||||
func getTLSConfig(ln net.Listener) *tls.Config {
|
||||
// Get listener type
|
||||
|
@ -263,33 +273,89 @@ func acceptsOfferType(spec, offerType string) bool {
|
|||
func getOffer(header string, isAccepted func(spec, offer string) bool, offers ...string) string {
|
||||
if len(offers) == 0 {
|
||||
return ""
|
||||
} else if header == "" {
|
||||
}
|
||||
if header == "" {
|
||||
return offers[0]
|
||||
}
|
||||
|
||||
for _, offer := range offers {
|
||||
if len(offer) == 0 {
|
||||
continue
|
||||
}
|
||||
spec, commaPos := "", 0
|
||||
for len(header) > 0 && commaPos != -1 {
|
||||
// Parse header and get accepted types with their quality and specificity
|
||||
// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields
|
||||
spec, commaPos, order := "", 0, 0
|
||||
acceptedTypes := make([]acceptedType, 0, 20)
|
||||
for len(header) > 0 {
|
||||
order++
|
||||
|
||||
// Skip spaces
|
||||
header = utils.TrimLeft(header, ' ')
|
||||
|
||||
// Get spec
|
||||
commaPos = strings.IndexByte(header, ',')
|
||||
if commaPos != -1 {
|
||||
spec = utils.Trim(header[:commaPos], ' ')
|
||||
} else {
|
||||
spec = utils.TrimLeft(header, ' ')
|
||||
}
|
||||
|
||||
// Get quality
|
||||
quality := 1.0
|
||||
if factorSign := strings.IndexByte(spec, ';'); factorSign != -1 {
|
||||
factor := utils.Trim(spec[factorSign+1:], ' ')
|
||||
if strings.HasPrefix(factor, "q=") {
|
||||
if q, err := fasthttp.ParseUfloat(utils.UnsafeBytes(factor[2:])); err == nil {
|
||||
quality = q
|
||||
}
|
||||
}
|
||||
spec = spec[:factorSign]
|
||||
}
|
||||
|
||||
// isAccepted if the current offer is accepted
|
||||
if isAccepted(spec, offer) {
|
||||
return offer
|
||||
}
|
||||
|
||||
// Skip if quality is 0.0
|
||||
// See: https://www.rfc-editor.org/rfc/rfc9110#quality.values
|
||||
if quality == 0.0 {
|
||||
if commaPos != -1 {
|
||||
header = header[commaPos+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get specificity
|
||||
specificity := 0
|
||||
// check for wildcard this could be a mime */* or a wildcard character *
|
||||
if spec == "*/*" || spec == "*" {
|
||||
specificity = 1
|
||||
} else if strings.HasSuffix(spec, "/*") {
|
||||
specificity = 2
|
||||
} else if strings.IndexByte(spec, '/') != -1 {
|
||||
specificity = 3
|
||||
} else {
|
||||
specificity = 4
|
||||
}
|
||||
|
||||
// Add to accepted types
|
||||
acceptedTypes = append(acceptedTypes, acceptedType{spec, quality, specificity, order})
|
||||
|
||||
// Next
|
||||
if commaPos != -1 {
|
||||
header = header[commaPos+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(acceptedTypes) > 1 {
|
||||
// Sort accepted types by quality and specificity, preserving order of equal elements
|
||||
sortAcceptedTypes(&acceptedTypes)
|
||||
}
|
||||
|
||||
// Find the first offer that matches the accepted types
|
||||
for _, acceptedType := range acceptedTypes {
|
||||
for _, offer := range offers {
|
||||
if len(offer) == 0 {
|
||||
continue
|
||||
}
|
||||
if isAccepted(acceptedType.spec, offer) {
|
||||
return offer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,6 +363,35 @@ func getOffer(header string, isAccepted func(spec, offer string) bool, offers ..
|
|||
return ""
|
||||
}
|
||||
|
||||
// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements
|
||||
//
|
||||
// Parameters are not supported, they are ignored when sorting by specificity.
|
||||
//
|
||||
// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields
|
||||
func sortAcceptedTypes(at *[]acceptedType) {
|
||||
if at == nil || len(*at) < 2 {
|
||||
return
|
||||
}
|
||||
acceptedTypes := *at
|
||||
|
||||
for i := 1; i < len(acceptedTypes); i++ {
|
||||
lo, hi := 0, i-1
|
||||
for lo <= hi {
|
||||
mid := (lo + hi) / 2
|
||||
if acceptedTypes[i].quality < acceptedTypes[mid].quality ||
|
||||
(acceptedTypes[i].quality == acceptedTypes[mid].quality && acceptedTypes[i].specificity < acceptedTypes[mid].specificity) ||
|
||||
(acceptedTypes[i].quality == acceptedTypes[mid].quality && acceptedTypes[i].specificity == acceptedTypes[mid].specificity && acceptedTypes[i].order > acceptedTypes[mid].order) {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid - 1
|
||||
}
|
||||
}
|
||||
for j := i; j > lo; j-- {
|
||||
acceptedTypes[j-1], acceptedTypes[j] = acceptedTypes[j], acceptedTypes[j-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func matchEtag(s, etag string) bool {
|
||||
if s == etag || s == "W/"+etag || "W/"+s == etag {
|
||||
return true
|
||||
|
|
129
helpers_test.go
129
helpers_test.go
|
@ -62,6 +62,128 @@ func Test_Utils_ETag(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func Test_Utils_GetOffer(t *testing.T) {
|
||||
t.Parallel()
|
||||
utils.AssertEqual(t, "", getOffer("hello", acceptsOffer))
|
||||
utils.AssertEqual(t, "1", getOffer("", acceptsOffer, "1"))
|
||||
utils.AssertEqual(t, "", getOffer("2", acceptsOffer, "1"))
|
||||
|
||||
utils.AssertEqual(t, "", getOffer("", acceptsOfferType))
|
||||
utils.AssertEqual(t, "", getOffer("text/html", acceptsOfferType))
|
||||
utils.AssertEqual(t, "", getOffer("text/html", acceptsOfferType, "application/json"))
|
||||
utils.AssertEqual(t, "", getOffer("text/html;q=0", acceptsOfferType, "text/html"))
|
||||
utils.AssertEqual(t, "", getOffer("application/json, */*; q=0", acceptsOfferType, "image/png"))
|
||||
utils.AssertEqual(t, "application/xml", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "application/xml", "application/json"))
|
||||
utils.AssertEqual(t, "text/html", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "text/html"))
|
||||
utils.AssertEqual(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json"))
|
||||
utils.AssertEqual(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json"))
|
||||
|
||||
utils.AssertEqual(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer))
|
||||
utils.AssertEqual(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "ascii"))
|
||||
utils.AssertEqual(t, "utf-8", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "utf-8"))
|
||||
utils.AssertEqual(t, "iso-8859-1", getOffer("utf-8;q=0, iso-8859-1;q=0.5", acceptsOffer, "utf-8", "iso-8859-1"))
|
||||
|
||||
utils.AssertEqual(t, "deflate", getOffer("gzip, deflate", acceptsOffer, "deflate"))
|
||||
utils.AssertEqual(t, "", getOffer("gzip, deflate;q=0", acceptsOffer, "deflate"))
|
||||
}
|
||||
|
||||
func Benchmark_Utils_GetOffer(b *testing.B) {
|
||||
headers := []string{
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"application/json",
|
||||
"utf-8, iso-8859-1;q=0.5",
|
||||
"gzip, deflate",
|
||||
}
|
||||
offers := [][]string{
|
||||
{"text/html", "application/xml", "application/xml+xhtml"},
|
||||
{"application/json"},
|
||||
{"utf-8"},
|
||||
{"deflate"},
|
||||
}
|
||||
for n := 0; n < b.N; n++ {
|
||||
for i, header := range headers {
|
||||
getOffer(header, acceptsOfferType, offers[i]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Utils_SortAcceptedTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
acceptedTypes := []acceptedType{
|
||||
{spec: "text/html", quality: 1, specificity: 3, order: 0},
|
||||
{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
|
||||
{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
|
||||
{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
|
||||
{spec: "application/xml", quality: 1, specificity: 3, order: 4},
|
||||
{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
|
||||
{spec: "image/png", quality: 1, specificity: 3, order: 6},
|
||||
{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
|
||||
{spec: "image/*", quality: 1, specificity: 2, order: 8},
|
||||
{spec: "image/gif", quality: 1, specificity: 3, order: 9},
|
||||
{spec: "text/plain", quality: 1, specificity: 3, order: 10},
|
||||
}
|
||||
sortAcceptedTypes(&acceptedTypes)
|
||||
utils.AssertEqual(t, acceptedTypes, []acceptedType{
|
||||
{spec: "text/html", quality: 1, specificity: 3, order: 0},
|
||||
{spec: "application/xml", quality: 1, specificity: 3, order: 4},
|
||||
{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
|
||||
{spec: "image/png", quality: 1, specificity: 3, order: 6},
|
||||
{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
|
||||
{spec: "image/gif", quality: 1, specificity: 3, order: 9},
|
||||
{spec: "text/plain", quality: 1, specificity: 3, order: 10},
|
||||
{spec: "image/*", quality: 1, specificity: 2, order: 8},
|
||||
{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
|
||||
{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
|
||||
{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
|
||||
})
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4
|
||||
func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
|
||||
acceptedTypes := make([]acceptedType, 3)
|
||||
for n := 0; n < b.N; n++ {
|
||||
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0}
|
||||
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1}
|
||||
acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2}
|
||||
sortAcceptedTypes(&acceptedTypes)
|
||||
}
|
||||
utils.AssertEqual(b, "text/html", acceptedTypes[0].spec)
|
||||
utils.AssertEqual(b, "text/*", acceptedTypes[1].spec)
|
||||
utils.AssertEqual(b, "*/*", acceptedTypes[2].spec)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4
|
||||
func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {
|
||||
acceptedTypes := make([]acceptedType, 11)
|
||||
for n := 0; n < b.N; n++ {
|
||||
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0}
|
||||
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1}
|
||||
acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2}
|
||||
acceptedTypes[3] = acceptedType{spec: "application/json", quality: 0.999, specificity: 3, order: 3}
|
||||
acceptedTypes[4] = acceptedType{spec: "application/xml", quality: 1, specificity: 3, order: 4}
|
||||
acceptedTypes[5] = acceptedType{spec: "application/pdf", quality: 1, specificity: 3, order: 5}
|
||||
acceptedTypes[6] = acceptedType{spec: "image/png", quality: 1, specificity: 3, order: 6}
|
||||
acceptedTypes[7] = acceptedType{spec: "image/jpeg", quality: 1, specificity: 3, order: 7}
|
||||
acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8}
|
||||
acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9}
|
||||
acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10}
|
||||
sortAcceptedTypes(&acceptedTypes)
|
||||
}
|
||||
utils.AssertEqual(b, acceptedTypes, []acceptedType{
|
||||
{spec: "text/html", quality: 1, specificity: 3, order: 0},
|
||||
{spec: "application/xml", quality: 1, specificity: 3, order: 4},
|
||||
{spec: "application/pdf", quality: 1, specificity: 3, order: 5},
|
||||
{spec: "image/png", quality: 1, specificity: 3, order: 6},
|
||||
{spec: "image/jpeg", quality: 1, specificity: 3, order: 7},
|
||||
{spec: "image/gif", quality: 1, specificity: 3, order: 9},
|
||||
{spec: "text/plain", quality: 1, specificity: 3, order: 10},
|
||||
{spec: "image/*", quality: 1, specificity: 2, order: 8},
|
||||
{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
|
||||
{spec: "text/*", quality: 0.5, specificity: 2, order: 1},
|
||||
{spec: "*/*", quality: 0.1, specificity: 1, order: 2},
|
||||
})
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_App_ETag -benchmem -count=4
|
||||
func Benchmark_Utils_ETag(b *testing.B) {
|
||||
app := New()
|
||||
|
@ -221,13 +343,6 @@ func Test_Utils_Parse_Address(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_Utils_GetOffset(t *testing.T) {
|
||||
t.Parallel()
|
||||
utils.AssertEqual(t, "", getOffer("hello", acceptsOffer))
|
||||
utils.AssertEqual(t, "1", getOffer("", acceptsOffer, "1"))
|
||||
utils.AssertEqual(t, "", getOffer("2", acceptsOffer, "1"))
|
||||
}
|
||||
|
||||
func Test_Utils_TestConn_Deadline(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn := &testConn{}
|
||||
|
|
Loading…
Reference in New Issue