Skip to content
Open
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
48 changes: 42 additions & 6 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const (
//go:generate ifacemaker --file ctx.go --file req.go --file res.go --struct DefaultCtx --iface Ctx --pkg fiber --promoted --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on."
type DefaultCtx struct {
handlerCtx CustomCtx // Active custom context implementation, if any
cancelFunc context.CancelFunc // Context cancel function
DefaultReq // Default request api
DefaultRes // Default response api
app *App // Reference to *App
Expand Down Expand Up @@ -136,15 +137,33 @@ func (c *DefaultCtx) Context() context.Context {
if c.fasthttp == nil {
return context.Background()
}
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx

ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context)
if !ok || ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(context.Background())
c.cancelFunc = cancel
c.SetContext(ctx)
}

if c.cancelFunc != nil && c.fasthttp.Conn() != nil {
fastHTTPDone := c.fasthttp.Done()
reqCtx := ctx
cancel := c.cancelFunc
c.cancelFunc = nil

go func() {
select {
case <-fastHTTPDone:
cancel()
case <-reqCtx.Done():
}
}()
}
ctx := context.Background()
c.SetContext(ctx)

return ctx
}
Comment thread
gaby marked this conversation as resolved.

// SetContext sets a context implementation by user.
func (c *DefaultCtx) SetContext(ctx context.Context) {
if c.fasthttp == nil {
return
Expand Down Expand Up @@ -685,6 +704,10 @@ func (c *DefaultCtx) configDependentPaths() {
// Reset is a method to reset context fields by given request when to use server handlers.
func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {
// Reset route and handler index
if c.cancelFunc != nil {
c.cancelFunc()
c.cancelFunc = nil
}
c.indexRoute = -1
c.indexHandler = 0
// Reset matched flag
Expand All @@ -694,9 +717,18 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {
// Set paths
c.pathOriginal = c.app.toString(fctx.URI().PathOriginal())
// Set method
c.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method()))
if fctx != nil {
c.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method()))
}
// Attach *fasthttp.RequestCtx to ctx
c.fasthttp = fctx
if fctx != nil {
reqCtx, cancel := context.WithCancel(context.Background())
c.cancelFunc = cancel

c.fasthttp.SetUserValue(userContextKey, reqCtx)
c.isUserContextSet = true
}
Comment thread
gaby marked this conversation as resolved.
// reset base uri
c.baseURI = ""
// Prettify path
Expand All @@ -708,6 +740,10 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {

// release is a method to reset context fields when to use ReleaseCtx()
func (c *DefaultCtx) release() {
if c.cancelFunc != nil {
c.cancelFunc()
c.cancelFunc = nil
}
if c.isUserContextSet {
if c.fasthttp != nil {
c.fasthttp.SetUserValue(userContextKey, nil)
Expand Down
120 changes: 119 additions & 1 deletion ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4211,7 +4211,12 @@ func Test_Ctx_Context(t *testing.T) {
t.Run("Nil_Context", func(t *testing.T) {
t.Parallel()
ctx := c.Context()
require.Equal(t, ctx, context.Background())

require.NotNil(t, ctx)
require.NoError(t, ctx.Err())

_, hasDeadline := ctx.Deadline()
require.False(t, hasDeadline)
})

t.Run("ValueContext", func(t *testing.T) {
Expand Down Expand Up @@ -9956,3 +9961,116 @@ func Benchmark_Ctx_OverrideParam(b *testing.B) {
c.OverrideParam("name", "changed")
}
}

// go test -v -run=Test_Ctx_Context_Lazy_Initialization_Suite
func Test_Ctx_Context_Lazy_Initialization_Suite(t *testing.T) {
app := New()

// Scenario with fake network connection
t.Run("Lazy_Initialization_With_Conn", func(t *testing.T) {
fctx := &fasthttp.RequestCtx{}
remoteAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}
fctx.Init(&fasthttp.Request{}, remoteAddr, nil)

ctx := app.AcquireCtx(fctx)
c, ok := ctx.(*DefaultCtx)
if !ok {
t.Fatal("AcquireCtx did not return *DefaultCtx")
}

if c.cancelFunc == nil {
t.Fatal("expected cancelFunc to be initialized")
}

goCtx := c.Context()
if goCtx == nil {
t.Fatal("expected Go context to be returned")
}
if c.cancelFunc != nil {
t.Error("expected cancelFunc to be nil after lazy initialization")
}

goCtx2 := c.Context()
if goCtx2 != goCtx {
t.Error("expected the same context instance on sequential calls")
}

app.ReleaseCtx(c)
})
// Scenario without fake network connection
t.Run("Lazy_Initialization_Without_Conn", func(t *testing.T) {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
c, ok := ctx.(*DefaultCtx)
if !ok {
t.Fatal("AcquireCtx did not return *DefaultCtx")
}

if c.cancelFunc == nil {
t.Fatal("expected cancelFunc to be initialized even without connection")
}

_ = c.Context()

if c.cancelFunc == nil {
t.Error("expected cancelFunc to remain non-nil when c.fasthttp.Conn() is nil")
}

app.ReleaseCtx(c)
})

// Scenario without .Context()
t.Run("Release_Without_Context_Access", func(t *testing.T) {
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
c, ok := ctx.(*DefaultCtx)
if !ok {
t.Fatal("AcquireCtx did not return *DefaultCtx")
}

if c.cancelFunc == nil {
t.Fatal("expected cancelFunc to be initialized")
}

app.ReleaseCtx(c)

if c.cancelFunc != nil {
t.Error("expected cancelFunc to be cleared by release()")
}
})
}

// go test -v -run=Test_Ctx_Context_Cancel_On_Disconnect
func Test_Ctx_Context_Cancel_On_Disconnect(t *testing.T) {
app := New()
fctx := &fasthttp.RequestCtx{}
netCtx, cancelNet := context.WithCancel(context.Background())

ctx := app.AcquireCtx(fctx)
c, ok := ctx.(*DefaultCtx)
if !ok {
t.Fatal("AcquireCtx did not return *DefaultCtx")
}

c.SetContext(netCtx)

goCtx := c.Context()

select {
case <-goCtx.Done():
t.Fatal("context should not be canceled yet")
default:
}

// close net connection
cancelNet()

select {
case <-goCtx.Done():
if !errors.Is(goCtx.Err(), context.Canceled) {
t.Errorf("expected context.Canceled, got %v", goCtx.Err())
}
case <-time.After(200 * time.Millisecond):
t.Fatal("expected Go context to be canceled after connection close")
}
Comment thread
gaby marked this conversation as resolved.

app.ReleaseCtx(c)
}
Comment thread
gaby marked this conversation as resolved.
69 changes: 69 additions & 0 deletions middleware/timeout/timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,72 @@ func TestTimeout_AbandonWithoutReclaimNotPooled(t *testing.T) {
app.ReleaseCtx(ctx)
require.True(t, ctx.IsAbandoned(), "abandoned, un-armed context must not be auto-reclaimed")
}

// TestTimeout_Integration_WithLazyContext verifies that the core's lazy context
// activation works seamlessly during a standard timeout event.
func TestTimeout_Integration_WithLazyContext(t *testing.T) {
t.Parallel()
app := fiber.New()

app.Get("/lazy-timeout", New(func(c fiber.Ctx) error {
_ = c.Context()

time.Sleep(50 * time.Millisecond)

select {
case <-c.Context().Done():
return c.Context().Err()
default:
return c.SendString("Should timeout")
}
}, Config{Timeout: 10 * time.Millisecond}))

req := httptest.NewRequest(fiber.MethodGet, "/lazy-timeout", http.NoBody)
resp, err := app.Test(req)

require.NoError(t, err)
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)
}

func TestTimeout_Integration_PanicAfterTimeoutWithLazyCtx(t *testing.T) {
t.Parallel()
app := fiber.New()

ctxCh := make(chan fiber.Ctx, 1)
release := make(chan struct{})
panicDone := make(chan struct{})

app.Get("/lazy-panic", New(func(c fiber.Ctx) error {
_ = c.Context()
ctxCh <- c
<-release

defer close(panicDone)
panic("disaster after timeout")
}, Config{Timeout: 10 * time.Millisecond}))

req := httptest.NewRequest(fiber.MethodGet, "/lazy-panic", http.NoBody)
resp, err := app.Test(req)

require.NoError(t, err)
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)

var c fiber.Ctx
select {
case c = <-ctxCh:
case <-time.After(time.Second):
t.Fatal("handler did not start")
}

close(release)

select {
case <-panicDone:
case <-time.After(time.Second):
t.Fatal("handler did not finish panic recovery")
}

require.Eventually(t, func() bool {
return !c.IsAbandoned()
}, time.Second, 5*time.Millisecond, "lazy context was not reclaimed properly after post-timeout panic")
}
Loading