Skip to content

Commit c53d865

Browse files
committed
fix(adaptor): preserve multi-value headers in net/http conversion
The header copy loops in handlerFunc and HTTPMiddleware called Header.Set once per value, so repeated header lines (e.g. two X-Forwarded-For entries) collapsed to the last value. Set the first value — keeping replace semantics for keys already present on the target request — and Add the rest. fasthttp's Add intentionally keeps singleton semantics for Cookie/Content-Type/etc., so those behave as before. New tests pin both conversion paths and fail against the old code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5sm1LHEnvEVDRGsHWFfKK
1 parent e1ed504 commit c53d865

2 files changed

Lines changed: 77 additions & 6 deletions

File tree

middleware/adaptor/adaptor.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,16 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler {
210210

211211
// Remove all cookies before setting, see https://github.com/valyala/fasthttp/pull/1864
212212
c.Request().Header.DelAllCookies()
213-
for key, val := range r.Header {
214-
for _, v := range val {
215-
c.Request().Header.Set(key, v)
213+
for key, vals := range r.Header {
214+
if len(vals) == 0 {
215+
continue
216+
}
217+
// Set replaces whatever the key held on the fiber request,
218+
// then Add appends the remaining values so multi-value
219+
// headers survive instead of collapsing to the last value.
220+
c.Request().Header.Set(key, vals[0])
221+
for _, v := range vals[1:] {
222+
c.Request().Header.Add(key, v)
216223
}
217224
}
218225
CopyContextToFiberContext(r.Context(), c.RequestCtx())
@@ -386,9 +393,19 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc {
386393
}
387394
req.Header.SetProtocol(proto)
388395

389-
for key, val := range r.Header {
390-
for _, v := range val {
391-
req.Header.Set(key, v)
396+
for key, vals := range r.Header {
397+
if len(vals) == 0 {
398+
continue
399+
}
400+
// Set replaces any value fasthttp derived while building the
401+
// request, then Add appends the remaining values so multi-value
402+
// headers (e.g. repeated X-Forwarded-For lines) survive instead
403+
// of collapsing to the last value. fasthttp's Add keeps its own
404+
// singleton semantics for Cookie/Content-Type/etc., which can
405+
// only hold one value there by design.
406+
req.Header.Set(key, vals[0])
407+
for _, v := range vals[1:] {
408+
req.Header.Add(key, v)
392409
}
393410
}
394411

middleware/adaptor/adaptor_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,3 +1834,57 @@ func Test_ResolveRemoteAddr_FastPathEquivalence(t *testing.T) {
18341834
require.Equal(t, want.String(), got.String(), "String() for %q", addr)
18351835
}
18361836
}
1837+
1838+
// Test_FiberHandlerFunc_MultiValueHeaders verifies repeated header lines on
1839+
// the net/http request all reach the fiber handler instead of collapsing to
1840+
// the last value.
1841+
func Test_FiberHandlerFunc_MultiValueHeaders(t *testing.T) {
1842+
t.Parallel()
1843+
1844+
var got []string
1845+
h := FiberHandlerFunc(func(c fiber.Ctx) error {
1846+
for _, v := range c.Request().Header.PeekAll("X-Multi") {
1847+
got = append(got, string(v))
1848+
}
1849+
return c.SendStatus(fiber.StatusOK)
1850+
})
1851+
1852+
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
1853+
r.Header.Add("X-Multi", "one")
1854+
r.Header.Add("X-Multi", "two")
1855+
r.Header.Add("X-Multi", "three")
1856+
w := httptest.NewRecorder()
1857+
h.ServeHTTP(w, r)
1858+
1859+
require.Equal(t, []string{"one", "two", "three"}, got)
1860+
}
1861+
1862+
// Test_HTTPMiddleware_MultiValueHeaders verifies multi-value headers added
1863+
// by a wrapped net/http middleware survive the copy-back onto the fiber
1864+
// request.
1865+
func Test_HTTPMiddleware_MultiValueHeaders(t *testing.T) {
1866+
t.Parallel()
1867+
1868+
mw := func(next http.Handler) http.Handler {
1869+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1870+
r.Header.Add("X-Multi", "one")
1871+
r.Header.Add("X-Multi", "two")
1872+
next.ServeHTTP(w, r)
1873+
})
1874+
}
1875+
1876+
app := fiber.New()
1877+
app.Use(HTTPMiddleware(mw))
1878+
var got []string
1879+
app.Get("/", func(c fiber.Ctx) error {
1880+
for _, v := range c.Request().Header.PeekAll("X-Multi") {
1881+
got = append(got, string(v))
1882+
}
1883+
return c.SendStatus(fiber.StatusOK)
1884+
})
1885+
1886+
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", http.NoBody))
1887+
require.NoError(t, err)
1888+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
1889+
require.Equal(t, []string{"one", "two"}, got)
1890+
}

0 commit comments

Comments
 (0)