Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion client/cookiejar.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,17 @@ func isIPLiteral(host string) bool {
host = host[1 : len(host)-1]
}

return net.ParseIP(host) != nil
// Equivalent to net.ParseIP(host) != nil: utils.ParseIPv4/ParseIPv6
// accept the same strings once zoned addresses (which net.ParseIP
// rejects) are screened out, without allocating on either outcome.
if strings.IndexByte(host, '%') >= 0 {
return false
}
if _, ok := utils.ParseIPv4(host); ok {
return true
}
_, ok := utils.ParseIPv6(host)
return ok
}

func isPublicSuffixDomain(domain string) bool {
Expand Down
23 changes: 23 additions & 0 deletions client/cookiejar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,29 @@ func Test_CookieJar_HostPort(t *testing.T) {
require.Equal(t, "fasthttp.com", string(cookies[0].Domain()))
}

func Test_CookieJar_isIPLiteral(t *testing.T) {
t.Parallel()

// Pinned to net.ParseIP semantics: zoned addresses are rejected.
tests := []struct {
host string
want bool
}{
{"192.0.2.1", true},
{"::1", true},
{"[::1]", true},
{"::ffff:192.0.2.1", true},
{"fe80::1%eth0", false},
{"[fe80::1%eth0]", false},
{"example.com", false},
{"192.0.2.256", false},
{"", false},
}
for _, tt := range tests {
require.Equal(t, tt.want, isIPLiteral(tt.host), "isIPLiteral(%q)", tt.host)
}
}

func Test_CookieJar_Domain(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2957,6 +2957,7 @@ func Test_Ctx_Fresh_ObsoleteDateFormats(t *testing.T) {
"Wed, 21 Oct 2015 07:28:00 GMT", // IMF-fixdate
"Wednesday, 21-Oct-15 07:28:00 GMT", // obsolete RFC 850 format
"Wed Oct 21 07:28:00 2015", // ANSI C asctime() format
" Wed, 21 Oct 2015 07:28:00 GMT ", // surrounding OWS tolerated (RFC 9110 §5.5)
} {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set(HeaderIfModifiedSince, date)
Expand Down
122 changes: 95 additions & 27 deletions middleware/adaptor/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,25 @@ func (*noopConn) SetDeadline(time.Time) error { return nil }
func (*noopConn) SetReadDeadline(time.Time) error { return nil }
func (*noopConn) SetWriteDeadline(time.Time) error { return nil }

// pooledCtx bundles the RequestCtx with its noopConn so one pool entry
// serves both and no per-request conn allocation is needed. The conn
// comes first to keep the struct's trailing pointer span minimal
// (govet fieldalignment).
type pooledCtx struct {
conn noopConn
fctx fasthttp.RequestCtx
}

var ctxPool = sync.Pool{
New: func() any {
return new(fasthttp.RequestCtx)
return new(pooledCtx)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
}

// disabledLogger is shared: it is stateless, and reusing one value keeps
// the logger interface conversion off the per-request path.
var disabledLogger = &disableLogger{}

// LocalContextKey is the key used to store the user's context.Context in the fasthttp request context.
// Adapted http.Handler functions can retrieve this context using r.Context().Value(adaptor.LocalContextKey)
var localContextKey = &struct{}{}
Expand Down Expand Up @@ -199,9 +212,16 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler {

// Remove all cookies before setting, see https://github.com/valyala/fasthttp/pull/1864
c.Request().Header.DelAllCookies()
for key, val := range r.Header {
for _, v := range val {
c.Request().Header.Set(key, v)
for key, vals := range r.Header {
if len(vals) == 0 {
continue
}
// Set replaces whatever the key held on the fiber request,
// then Add appends the remaining values so multi-value
// headers survive instead of collapsing to the last value.
c.Request().Header.Set(key, vals[0])
for _, v := range vals[1:] {
c.Request().Header.Add(key, v)
}
}
CopyContextToFiberContext(r.Context(), c.RequestCtx())
Expand Down Expand Up @@ -237,6 +257,24 @@ func isUnixNetwork(network string) bool {
return network == "unix" || network == "unixgram" || network == "unixpacket"
}

// parseDecimalPort parses a purely numeric port string. Anything else —
// signs, service names like "http", overflow — reports ok=false so the
// caller falls back to net.ResolveTCPAddr, which handles the long tail.
func parseDecimalPort(s string) (int, bool) {
if s == "" || len(s) > 5 {
return 0, false
}
port := 0
for i := 0; i < len(s); i++ {
d := s[i] - '0'
if d > 9 {
return 0, false
}
port = port*10 + int(d)
}
return port, port <= 65535
}

func resolveRemoteAddr(remoteAddr string, localAddr any) (net.Addr, error) {
if addr, ok := localAddr.(net.Addr); ok && isUnixNetwork(addr.Network()) {
return addr, nil
Expand All @@ -247,6 +285,23 @@ func resolveRemoteAddr(remoteAddr string, localAddr any) (net.Addr, error) {
return nil, ErrRemoteAddrEmpty
}

// Fast path: "ip:port" literals — the only form net/http servers set on
// Request.RemoteAddr — build the TCPAddr directly instead of going
// through net.ResolveTCPAddr's resolver machinery. Hostnames, service
// port names, and anything else fall through to the resolver below.
if host, portStr, err := net.SplitHostPort(remoteAddr); err == nil {
if port, ok := parseDecimalPort(portStr); ok {
if ip, ok := utils.ParseIPv4(host); ok {
a := ip.As4()
return &net.TCPAddr{IP: a[:], Port: port}, nil
}
if ip, ok := utils.ParseIPv6(host); ok {
a := ip.As16()
return &net.TCPAddr{IP: a[:], Port: port, Zone: ip.Zone()}, nil
}
}
}

resolved, err := net.ResolveTCPAddr("tcp", remoteAddr)
if err == nil {
return resolved, nil
Expand All @@ -269,8 +324,29 @@ func resolveRemoteAddr(remoteAddr string, localAddr any) (net.Addr, error) {

func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
// New fasthttp Ctx from pool
pctx := ctxPool.Get().(*pooledCtx) //nolint:forcetypeassert,errcheck // not needed
fctx := &pctx.fctx
fctx.Response.Reset()
fctx.Request.Reset()
defer ctxPool.Put(pctx)

remoteAddr, err := resolveRemoteAddr(r.RemoteAddr, r.Context().Value(http.LocalAddrContextKey))
if err != nil {
remoteAddr = nil // Fallback to nil
}
pctx.conn.remoteAddr = remoteAddr

// Init2 mirrors fasthttp's RequestCtx.Init, but with a no-op
// connection instead of fasthttp's fakeAddrer, whose Write panics.
// Interim responses (e.g. SendEarlyHints' 103) are then silently
// discarded instead of panicking; the final response still reaches
// the client through the ResponseWriter copy-back below. Init2 only
// touches connection metadata and buffer-retention flags, so the
// request is built directly into fctx.Request afterwards — the same
// order fasthttp's Init uses, minus its full request copy.
fctx.Init2(&pctx.conn, disabledLogger, true)
req := &fctx.Request

// Convert net/http -> fasthttp request with size limit
maxBodySize := int64(app.Config().BodyLimit)
Expand Down Expand Up @@ -319,30 +395,22 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc {
}
req.Header.SetProtocol(proto)

for key, val := range r.Header {
for _, v := range val {
req.Header.Set(key, v)
for key, vals := range r.Header {
if len(vals) == 0 {
continue
}
// Set replaces any value fasthttp derived while building the
// request, then Add appends the remaining values so multi-value
// headers (e.g. repeated X-Forwarded-For lines) survive instead
// of collapsing to the last value. fasthttp's Add keeps its own
// singleton semantics for Cookie/Content-Type/etc., which can
// only hold one value there by design.
req.Header.Set(key, vals[0])
for _, v := range vals[1:] {
req.Header.Add(key, v)
}
}

remoteAddr, err := resolveRemoteAddr(r.RemoteAddr, r.Context().Value(http.LocalAddrContextKey))
if err != nil {
remoteAddr = nil // Fallback to nil
}

// New fasthttp Ctx from pool
fctx := ctxPool.Get().(*fasthttp.RequestCtx) //nolint:forcetypeassert,errcheck // not needed
fctx.Response.Reset()
fctx.Request.Reset()
defer ctxPool.Put(fctx)
// Init2 + CopyTo mirror fasthttp's RequestCtx.Init, but with a no-op
// connection instead of fasthttp's fakeAddrer, whose Write panics.
// Interim responses (e.g. SendEarlyHints' 103) are then silently
// discarded instead of panicking; the final response still reaches
// the client through the ResponseWriter copy-back below.
fctx.Init2(&noopConn{remoteAddr: remoteAddr}, &disableLogger{}, true)
req.CopyTo(&fctx.Request)

if len(h) > 0 {
// New fiber Ctx
ctx := app.AcquireCtx(fctx)
Expand Down
135 changes: 135 additions & 0 deletions middleware/adaptor/adaptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1778,3 +1778,138 @@ func TestUnixSocketAdaptor(t *testing.T) {
t.Fatal("server shutdown timed out")
}
}

// Test_ResolveRemoteAddr_FastPathEquivalence pins the ip:port fast path to
// net.ResolveTCPAddr: for every literal form the fast path accepts, both
// must produce the same address, and inputs the fast path rejects must
// still resolve identically through the fallback.
func Test_ResolveRemoteAddr_FastPathEquivalence(t *testing.T) {
t.Parallel()

addrs := []string{
"1.2.3.4:6789",
"127.0.0.1:80",
"255.255.255.255:65535",
"[::1]:8080",
"[2001:db8::1]:443",
"[::ffff:1.2.3.4]:80",
"[fe80::1%eth0]:80",
"1.2.3.4:0",
}
for _, addr := range addrs {
got, err := resolveRemoteAddr(addr, nil)
require.NoError(t, err, "resolveRemoteAddr(%q)", addr)
want, err := net.ResolveTCPAddr("tcp", addr)
require.NoError(t, err, "ResolveTCPAddr(%q)", addr)
gotTCP, ok := got.(*net.TCPAddr)
require.True(t, ok, "resolveRemoteAddr(%q) type", addr)
require.True(t, want.IP.Equal(gotTCP.IP), "IP for %q: got %v want %v", addr, gotTCP.IP, want.IP)
require.Equal(t, want.Port, gotTCP.Port, "port for %q", addr)
require.Equal(t, want.Zone, gotTCP.Zone, "zone for %q", addr)
require.Equal(t, want.String(), gotTCP.String(), "String() for %q", addr)
}

// Rejected by the fast path, resolved by the fallback: identical results.
fallbackAddrs := []string{
"localhost:80", // hostname
"1.2.3.4:0080", // leading-zero port (fast path parses digits only, both accept)
"1.2.3.4", // missing port -> :80 default via fallback
"01.2.3.4:80", // leading-zero octet (rejected by ParseIPv4, accepted by resolver)
"1.2.3.4:99999", // port out of range -> error from both? fallback errors
"1.2.3.4:", // empty port -> fast path rejects, resolver yields port 0
"1.2.3.4:123456", // six digits -> fast path length guard, resolver errors
"1.2.3.4:8a", // non-digit port -> fast path rejects, resolver service lookup fails
}
for _, addr := range fallbackAddrs {
got, gotErr := resolveRemoteAddr(addr, nil)
want, wantErr := net.ResolveTCPAddr("tcp", addr)
if addr == "1.2.3.4" {
// resolveRemoteAddr appends :80 for missing ports; ResolveTCPAddr errors.
require.NoError(t, gotErr)
require.Equal(t, "1.2.3.4:80", got.String())
continue
}
if wantErr != nil {
require.Error(t, gotErr, "addr %q", addr)
continue
}
require.NoError(t, gotErr, "addr %q", addr)
require.Equal(t, want.String(), got.String(), "String() for %q", addr)
}
}

// Test_FiberHandlerFunc_MultiValueHeaders verifies repeated header lines on
// the net/http request all reach the fiber handler instead of collapsing to
// the last value.
func Test_FiberHandlerFunc_MultiValueHeaders(t *testing.T) {
t.Parallel()

var got []string
h := FiberHandlerFunc(func(c fiber.Ctx) error {
for _, v := range c.Request().Header.PeekAll("X-Multi") {
got = append(got, string(v))
}
return c.SendStatus(fiber.StatusOK)
})

r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
r.Header.Add("X-Multi", "one")
r.Header.Add("X-Multi", "two")
r.Header.Add("X-Multi", "three")
w := httptest.NewRecorder()
h.ServeHTTP(w, r)

require.Equal(t, []string{"one", "two", "three"}, got)
}

// Test_FiberHandlerFunc_EmptyHeaderValues verifies a header key mapped to an
// empty value slice is skipped instead of panicking or producing an empty
// header.
func Test_FiberHandlerFunc_EmptyHeaderValues(t *testing.T) {
t.Parallel()

var seen bool
h := FiberHandlerFunc(func(c fiber.Ctx) error {
seen = len(c.Request().Header.PeekAll("X-Empty")) > 0
return c.SendStatus(fiber.StatusOK)
})

r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
r.Header["X-Empty"] = []string{}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)

require.Equal(t, http.StatusOK, w.Code)
require.False(t, seen)
}

// Test_HTTPMiddleware_MultiValueHeaders verifies multi-value headers added
// by a wrapped net/http middleware survive the copy-back onto the fiber
// request.
func Test_HTTPMiddleware_MultiValueHeaders(t *testing.T) {
t.Parallel()

mw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Add("X-Multi", "one")
r.Header.Add("X-Multi", "two")
r.Header["X-Empty"] = nil // empty value slices must be skipped
next.ServeHTTP(w, r)
})
}

app := fiber.New()
app.Use(HTTPMiddleware(mw))
var got []string
app.Get("/", func(c fiber.Ctx) error {
for _, v := range c.Request().Header.PeekAll("X-Multi") {
got = append(got, string(v))
}
return c.SendStatus(fiber.StatusOK)
})

resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", http.NoBody))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, []string{"one", "two"}, got)
}
9 changes: 6 additions & 3 deletions middleware/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ func New(config ...Config) fiber.Handler {
c.Response().Header.SetBytesV(fiber.HeaderETag, e.etag)
}
clampedDate := clampDateSeconds(e.date, ts)
dateValue := fasthttp.AppendHTTPDate(nil, secondsToTime(clampedDate))
dateValue := utils.AppendHTTPDate(nil, secondsToTime(clampedDate))
c.Response().Header.SetBytesV(fiber.HeaderDate, dateValue)
for i := range e.headers {
h := e.headers[i]
Expand Down Expand Up @@ -715,7 +715,7 @@ func New(config ...Config) fiber.Handler {
dateHeader := c.Response().Header.Peek(fiber.HeaderDate)
parsedDate, _ := parseHTTPDate(dateHeader)
e.date = clampDateSeconds(parsedDate, nowUnix)
dateBytes := fasthttp.AppendHTTPDate(nil, secondsToTime(e.date))
dateBytes := utils.AppendHTTPDate(nil, secondsToTime(e.date))
c.Response().Header.SetBytesV(fiber.HeaderDate, dateBytes)

// Store all response headers
Expand Down Expand Up @@ -750,7 +750,10 @@ func New(config ...Config) fiber.Handler {
expiration = secondsToDuration(respCacheControl.maxAge)
expirationSource = expirationSourceMaxAge
} else if expiresBytes := c.Response().Header.Peek(fiber.HeaderExpires); len(expiresBytes) > 0 {
expiresAt, err := fasthttp.ParseHTTPDate(expiresBytes)
// Same parser as the Date header (utils.go parseHTTPDate) so
// both share one acceptance set: IMF-fixdate plus the obsolete
// RFC 850 and asctime forms RFC 9110 §5.6.7 requires.
expiresAt, err := utils.ParseHTTPDate(expiresBytes)
if err != nil {
expiration = time.Nanosecond
expiresParseError = true
Expand Down
Loading
Loading