Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 14 additions & 3 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2396,12 +2396,17 @@ func Benchmark_Client_Request_Send_ContextCancel(b *testing.B) {
app, ln, start := createHelperServer(b)

startedCh := make(chan struct{})
canceledCh := make(chan struct{})
errCh := make(chan error)
respCh := make(chan *Response)

app.Post("/", func(c fiber.Ctx) error {
startedCh <- struct{}{}
time.Sleep(time.Millisecond) // let cancel be called
// Hold the response until the loop has received Send's outcome:
// with the reply still pending, the client's response channel
// cannot be ready, so Send can only return through its
// ctx.Done() branch — no race against the reply.
<-canceledCh
return c.Status(fiber.StatusOK).SendString("post")
})

Expand Down Expand Up @@ -2433,8 +2438,14 @@ func Benchmark_Client_Request_Send_ContextCancel(b *testing.B) {
<-startedCh // request is made, we can cancel the context now
cancel()

require.Nil(b, <-respCh)
require.ErrorIs(b, <-errCh, ErrTimeoutOrCancel)
// Send must return before the handler is released below, so these
// receives can only observe the cancellation outcome.
resp := <-respCh
err := <-errCh
canceledCh <- struct{}{} // release the handler; core's drainer reclaims the late response

require.Nil(b, resp)
require.ErrorIs(b, err, ErrTimeoutOrCancel)
}
}

Expand Down
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
Loading