Skip to content

Commit b71be30

Browse files
authored
Merge branch 'main' into 2025-06-26-02-37-44
2 parents 3c27429 + 55818fb commit b71be30

File tree

5 files changed

+254
-6
lines changed

5 files changed

+254
-6
lines changed

ctx.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,22 +209,26 @@ type ResFmt struct {
209209

210210
// Accepts checks if the specified extensions or content types are acceptable.
211211
func (c *DefaultCtx) Accepts(offers ...string) string {
212-
return getOffer(c.fasthttp.Request.Header.Peek(HeaderAccept), acceptsOfferType, offers...)
212+
header := joinHeaderValues(c.fasthttp.Request.Header.PeekAll(HeaderAccept))
213+
return getOffer(header, acceptsOfferType, offers...)
213214
}
214215

215216
// AcceptsCharsets checks if the specified charset is acceptable.
216217
func (c *DefaultCtx) AcceptsCharsets(offers ...string) string {
217-
return getOffer(c.fasthttp.Request.Header.Peek(HeaderAcceptCharset), acceptsOffer, offers...)
218+
header := joinHeaderValues(c.fasthttp.Request.Header.PeekAll(HeaderAcceptCharset))
219+
return getOffer(header, acceptsOffer, offers...)
218220
}
219221

220222
// AcceptsEncodings checks if the specified encoding is acceptable.
221223
func (c *DefaultCtx) AcceptsEncodings(offers ...string) string {
222-
return getOffer(c.fasthttp.Request.Header.Peek(HeaderAcceptEncoding), acceptsOffer, offers...)
224+
header := joinHeaderValues(c.fasthttp.Request.Header.PeekAll(HeaderAcceptEncoding))
225+
return getOffer(header, acceptsOffer, offers...)
223226
}
224227

225228
// AcceptsLanguages checks if the specified language is acceptable.
226229
func (c *DefaultCtx) AcceptsLanguages(offers ...string) string {
227-
return getOffer(c.fasthttp.Request.Header.Peek(HeaderAcceptLanguage), acceptsOffer, offers...)
230+
header := joinHeaderValues(c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))
231+
return getOffer(header, acceptsOffer, offers...)
228232
}
229233

230234
// App returns the *App reference to the instance of the Fiber application

ctx_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,17 @@ func Test_Ctx_Accepts_Wildcard(t *testing.T) {
171171
require.Equal(t, "xml", c.Accepts("xml"))
172172
}
173173

174+
// go test -run Test_Ctx_Accepts_MultiHeader
175+
func Test_Ctx_Accepts_MultiHeader(t *testing.T) {
176+
t.Parallel()
177+
app := New()
178+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
179+
180+
c.Request().Header.Add(HeaderAccept, "text/plain;q=0.5")
181+
c.Request().Header.Add(HeaderAccept, "application/json")
182+
require.Equal(t, "application/json", c.Accepts("text/plain", "application/json"))
183+
}
184+
174185
// go test -run Test_Ctx_AcceptsCharsets
175186
func Test_Ctx_AcceptsCharsets(t *testing.T) {
176187
t.Parallel()
@@ -181,6 +192,17 @@ func Test_Ctx_AcceptsCharsets(t *testing.T) {
181192
require.Equal(t, "utf-8", c.AcceptsCharsets("utf-8"))
182193
}
183194

195+
// go test -run Test_Ctx_AcceptsCharsets_MultiHeader
196+
func Test_Ctx_AcceptsCharsets_MultiHeader(t *testing.T) {
197+
t.Parallel()
198+
app := New()
199+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
200+
201+
c.Request().Header.Add(HeaderAcceptCharset, "utf-8;q=0.1")
202+
c.Request().Header.Add(HeaderAcceptCharset, "iso-8859-1")
203+
require.Equal(t, "iso-8859-1", c.AcceptsCharsets("utf-8", "iso-8859-1"))
204+
}
205+
184206
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4
185207
func Benchmark_Ctx_AcceptsCharsets(b *testing.B) {
186208
app := New()
@@ -206,6 +228,17 @@ func Test_Ctx_AcceptsEncodings(t *testing.T) {
206228
require.Equal(t, "abc", c.AcceptsEncodings("abc"))
207229
}
208230

231+
// go test -run Test_Ctx_AcceptsEncodings_MultiHeader
232+
func Test_Ctx_AcceptsEncodings_MultiHeader(t *testing.T) {
233+
t.Parallel()
234+
app := New()
235+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
236+
237+
c.Request().Header.Add(HeaderAcceptEncoding, "deflate;q=0.3")
238+
c.Request().Header.Add(HeaderAcceptEncoding, "gzip")
239+
require.Equal(t, "gzip", c.AcceptsEncodings("deflate", "gzip"))
240+
}
241+
209242
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4
210243
func Benchmark_Ctx_AcceptsEncodings(b *testing.B) {
211244
app := New()
@@ -230,6 +263,17 @@ func Test_Ctx_AcceptsLanguages(t *testing.T) {
230263
require.Equal(t, "fr", c.AcceptsLanguages("fr"))
231264
}
232265

266+
// go test -run Test_Ctx_AcceptsLanguages_MultiHeader
267+
func Test_Ctx_AcceptsLanguages_MultiHeader(t *testing.T) {
268+
t.Parallel()
269+
app := New()
270+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
271+
272+
c.Request().Header.Add(HeaderAcceptLanguage, "de;q=0.4")
273+
c.Request().Header.Add(HeaderAcceptLanguage, "en")
274+
require.Equal(t, "en", c.AcceptsLanguages("de", "en"))
275+
}
276+
233277
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4
234278
func Benchmark_Ctx_AcceptsLanguages(b *testing.B) {
235279
app := New()

helpers.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ func paramsMatch(specParamStr headerParams, offerParams string) bool {
210210
fasthttp.VisitHeaderParams(utils.UnsafeBytes(offerParams), func(key, value []byte) bool {
211211
if utils.EqualFold(specParam, utils.UnsafeString(key)) {
212212
foundParam = true
213-
allSpecParamsMatch = utils.EqualFold(specVal, value)
213+
unescaped, err := unescapeHeaderValue(value)
214+
if err != nil {
215+
allSpecParamsMatch = false
216+
return false
217+
}
218+
allSpecParamsMatch = utils.EqualFold(specVal, unescaped)
214219
return false
215220
}
216221
return true
@@ -253,6 +258,45 @@ func getSplicedStrList(headerValue string, dst []string) []string {
253258
return dst
254259
}
255260

261+
func joinHeaderValues(headers [][]byte) []byte {
262+
switch len(headers) {
263+
case 0:
264+
return nil
265+
case 1:
266+
return headers[0]
267+
default:
268+
return bytes.Join(headers, []byte{','})
269+
}
270+
}
271+
272+
func unescapeHeaderValue(v []byte) ([]byte, error) {
273+
if bytes.IndexByte(v, '\\') == -1 {
274+
return v, nil
275+
}
276+
res := make([]byte, 0, len(v))
277+
escaping := false
278+
for i, c := range v {
279+
if escaping {
280+
res = append(res, c)
281+
escaping = false
282+
continue
283+
}
284+
if c == '\\' {
285+
// invalid escape at end of string
286+
if i == len(v)-1 {
287+
return nil, errors.New("invalid escape sequence")
288+
}
289+
escaping = true
290+
continue
291+
}
292+
res = append(res, c)
293+
}
294+
if escaping {
295+
return nil, errors.New("invalid escape sequence")
296+
}
297+
return res, nil
298+
}
299+
256300
// forEachMediaRange parses an Accept or Content-Type header, calling functor
257301
// on each media range.
258302
// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields
@@ -352,7 +396,11 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
352396
return false
353397
}
354398
lowerKey := utils.UnsafeString(utils.ToLowerBytes(key))
355-
params[lowerKey] = value
399+
val, err := unescapeHeaderValue(value)
400+
if err != nil {
401+
return true
402+
}
403+
params[lowerKey] = val
356404
return true
357405
})
358406
}

helpers_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,3 +1317,39 @@ func Benchmark_GenericParseTypeBoolean(b *testing.B) {
13171317
})
13181318
}
13191319
}
1320+
1321+
func Test_UnescapeHeaderValue(t *testing.T) {
1322+
t.Parallel()
1323+
cases := []struct {
1324+
in string
1325+
out []byte
1326+
ok bool
1327+
}{
1328+
{in: "abc", out: []byte("abc"), ok: true},
1329+
{in: "a\\\"b", out: []byte("a\"b"), ok: true},
1330+
{in: "c\\\\d", out: []byte("c\\d"), ok: true},
1331+
{in: "bad\\", ok: false},
1332+
}
1333+
for _, tc := range cases {
1334+
out, err := unescapeHeaderValue([]byte(tc.in))
1335+
if tc.ok {
1336+
require.NoError(t, err, tc.in)
1337+
require.Equal(t, tc.out, out, tc.in)
1338+
} else {
1339+
require.Error(t, err, tc.in)
1340+
}
1341+
}
1342+
}
1343+
1344+
func Test_JoinHeaderValues(t *testing.T) {
1345+
t.Parallel()
1346+
require.Nil(t, joinHeaderValues(nil))
1347+
require.Equal(t, []byte("a"), joinHeaderValues([][]byte{[]byte("a")}))
1348+
require.Equal(t, []byte("a,b"), joinHeaderValues([][]byte{[]byte("a"), []byte("b")}))
1349+
}
1350+
1351+
func Test_ParamsMatch_InvalidEscape(t *testing.T) {
1352+
t.Parallel()
1353+
match := paramsMatch(headerParams{"foo": []byte("bar")}, `;foo="bar\\`)
1354+
require.False(t, match)
1355+
}

router_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,3 +1304,119 @@ type routeJSON struct {
13041304
TestRoutes []testRoute `json:"test_routes"`
13051305
GithubAPI []testRoute `json:"github_api"`
13061306
}
1307+
1308+
func newCustomApp() *App {
1309+
return NewWithCustomCtx(func(app *App) CustomCtx {
1310+
return &customCtx{DefaultCtx: *NewDefaultCtx(app)}
1311+
})
1312+
}
1313+
1314+
func Test_NextCustom_MethodNotAllowed(t *testing.T) {
1315+
t.Parallel()
1316+
app := newCustomApp()
1317+
app.Get("/foo", func(c Ctx) error { return c.SendStatus(StatusOK) })
1318+
useRoute := &Route{use: true, path: "/foo", Path: "/foo", routeParser: parseRoute("/foo")}
1319+
m := app.methodInt(MethodGet)
1320+
app.stack[m] = append([]*Route{useRoute}, app.stack[m]...)
1321+
app.routesRefreshed = true
1322+
app.RebuildTree()
1323+
1324+
fctx := &fasthttp.RequestCtx{}
1325+
fctx.Request.Header.SetMethod(MethodPost)
1326+
fctx.Request.SetRequestURI("/foo")
1327+
1328+
ctx := app.AcquireCtx(fctx)
1329+
defer app.ReleaseCtx(ctx)
1330+
1331+
matched, err := app.nextCustom(ctx)
1332+
require.False(t, matched)
1333+
require.ErrorIs(t, err, ErrMethodNotAllowed)
1334+
allow := string(ctx.Response().Header.Peek(HeaderAllow))
1335+
require.Equal(t, MethodGet, allow)
1336+
}
1337+
1338+
func Test_NextCustom_NotFound(t *testing.T) {
1339+
t.Parallel()
1340+
app := newCustomApp()
1341+
app.RebuildTree()
1342+
1343+
fctx := &fasthttp.RequestCtx{}
1344+
fctx.Request.Header.SetMethod(MethodGet)
1345+
fctx.Request.SetRequestURI("/not-exist")
1346+
1347+
ctx := app.AcquireCtx(fctx)
1348+
defer app.ReleaseCtx(ctx)
1349+
1350+
matched, err := app.nextCustom(ctx)
1351+
require.False(t, matched)
1352+
var e *Error
1353+
require.ErrorAs(t, err, &e)
1354+
require.Equal(t, StatusNotFound, e.Code)
1355+
}
1356+
1357+
func Test_RequestHandler_CustomCtx_NotImplemented(t *testing.T) {
1358+
t.Parallel()
1359+
app := newCustomApp()
1360+
1361+
h := app.Handler()
1362+
fctx := &fasthttp.RequestCtx{}
1363+
fctx.Request.Header.SetMethod("UNKNOWN")
1364+
fctx.Request.SetRequestURI("/")
1365+
1366+
h(fctx)
1367+
require.Equal(t, StatusNotImplemented, fctx.Response.StatusCode())
1368+
}
1369+
1370+
func Test_NextCustom_Matched404(t *testing.T) {
1371+
t.Parallel()
1372+
app := newCustomApp()
1373+
app.RebuildTree()
1374+
1375+
fctx := &fasthttp.RequestCtx{}
1376+
fctx.Request.Header.SetMethod(MethodGet)
1377+
fctx.Request.SetRequestURI("/none")
1378+
1379+
ctx := app.AcquireCtx(fctx)
1380+
ctx.setMatched(true)
1381+
defer app.ReleaseCtx(ctx)
1382+
1383+
matched, err := app.nextCustom(ctx)
1384+
require.False(t, matched)
1385+
var e *Error
1386+
require.ErrorAs(t, err, &e)
1387+
require.Equal(t, StatusNotFound, e.Code)
1388+
}
1389+
1390+
func Test_NextCustom_SkipMountAndNoHandlers(t *testing.T) {
1391+
t.Parallel()
1392+
app := newCustomApp()
1393+
m := app.methodInt(MethodGet)
1394+
mountR := &Route{path: "/skip", Path: "/skip", routeParser: parseRoute("/skip"), mount: true}
1395+
empty := &Route{path: "/foo", Path: "/foo", routeParser: parseRoute("/foo")}
1396+
app.stack[m] = []*Route{mountR, empty}
1397+
app.routesRefreshed = true
1398+
app.RebuildTree()
1399+
1400+
fctx := &fasthttp.RequestCtx{}
1401+
fctx.Request.Header.SetMethod(MethodGet)
1402+
fctx.Request.SetRequestURI("/foo")
1403+
1404+
ctx := app.AcquireCtx(fctx)
1405+
defer app.ReleaseCtx(ctx)
1406+
1407+
matched, err := app.nextCustom(ctx)
1408+
require.True(t, matched)
1409+
require.NoError(t, err)
1410+
require.Equal(t, "/foo", ctx.Route().Path)
1411+
}
1412+
1413+
func Test_AddRoute_MergeHandlers(t *testing.T) {
1414+
t.Parallel()
1415+
app := New()
1416+
count := func(_ Ctx) error { return nil }
1417+
app.Get("/merge", count)
1418+
app.Get("/merge", count)
1419+
1420+
require.Len(t, app.stack[app.methodInt(MethodGet)], 1)
1421+
require.Len(t, app.stack[app.methodInt(MethodGet)][0].Handlers, 2)
1422+
}

0 commit comments

Comments
 (0)