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, "", c.Accepts())
|
||||||
utils.AssertEqual(t, ".xml", c.Accepts(".xml"))
|
utils.AssertEqual(t, ".xml", c.Accepts(".xml"))
|
||||||
utils.AssertEqual(t, "", c.Accepts(".john"))
|
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")
|
c.Request().Header.Set(HeaderAccept, "text/*, application/json")
|
||||||
utils.AssertEqual(t, "html", c.Accepts("html"))
|
utils.AssertEqual(t, "html", c.Accepts("html"))
|
||||||
|
|
|
@ -24,19 +24,31 @@ func (c *Ctx) AcceptsLanguages(offers ...string) string
|
||||||
```
|
```
|
||||||
|
|
||||||
```go title="Example"
|
```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 {
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
c.Accepts("html") // "html"
|
c.Accepts("html") // "html"
|
||||||
c.Accepts("text/html") // "text/html"
|
c.Accepts("text/html") // "text/html"
|
||||||
c.Accepts("json", "text") // "json"
|
c.Accepts("json", "text") // "json"
|
||||||
c.Accepts("application/json") // "application/json"
|
c.Accepts("application/json") // "application/json"
|
||||||
|
c.Accepts("text/plain", "application/json") // "application/json", due to quality
|
||||||
c.Accepts("image/png") // ""
|
c.Accepts("image/png") // ""
|
||||||
c.Accepts("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.
|
Fiber provides similar functions for the other accept headers.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
131
helpers.go
131
helpers.go
|
@ -25,6 +25,16 @@ import (
|
||||||
"github.com/valyala/fasthttp"
|
"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
|
// getTLSConfig returns a net listener's tls config
|
||||||
func getTLSConfig(ln net.Listener) *tls.Config {
|
func getTLSConfig(ln net.Listener) *tls.Config {
|
||||||
// Get listener type
|
// 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 {
|
func getOffer(header string, isAccepted func(spec, offer string) bool, offers ...string) string {
|
||||||
if len(offers) == 0 {
|
if len(offers) == 0 {
|
||||||
return ""
|
return ""
|
||||||
} else if header == "" {
|
}
|
||||||
|
if header == "" {
|
||||||
return offers[0]
|
return offers[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, offer := range offers {
|
// Parse header and get accepted types with their quality and specificity
|
||||||
if len(offer) == 0 {
|
// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields
|
||||||
continue
|
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, ' ')
|
||||||
}
|
}
|
||||||
spec, commaPos := "", 0
|
|
||||||
for len(header) > 0 && commaPos != -1 {
|
|
||||||
commaPos = strings.IndexByte(header, ',')
|
|
||||||
if commaPos != -1 {
|
|
||||||
spec = utils.Trim(header[:commaPos], ' ')
|
|
||||||
} else {
|
|
||||||
spec = utils.TrimLeft(header, ' ')
|
|
||||||
}
|
|
||||||
if factorSign := strings.IndexByte(spec, ';'); factorSign != -1 {
|
|
||||||
spec = spec[:factorSign]
|
|
||||||
}
|
|
||||||
|
|
||||||
// isAccepted if the current offer is accepted
|
// Get quality
|
||||||
if isAccepted(spec, offer) {
|
quality := 1.0
|
||||||
return offer
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if quality is 0.0
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc9110#quality.values
|
||||||
|
if quality == 0.0 {
|
||||||
if commaPos != -1 {
|
if commaPos != -1 {
|
||||||
header = header[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 ""
|
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 {
|
func matchEtag(s, etag string) bool {
|
||||||
if s == etag || s == "W/"+etag || "W/"+s == etag {
|
if s == etag || s == "W/"+etag || "W/"+s == etag {
|
||||||
return true
|
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
|
// go test -v -run=^$ -bench=Benchmark_App_ETag -benchmem -count=4
|
||||||
func Benchmark_Utils_ETag(b *testing.B) {
|
func Benchmark_Utils_ETag(b *testing.B) {
|
||||||
app := New()
|
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) {
|
func Test_Utils_TestConn_Deadline(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
conn := &testConn{}
|
conn := &testConn{}
|
||||||
|
|
Loading…
Reference in New Issue