Skip to content

🔥 feat: added cancelFunc and async goroutine to monitore lost of connec…#4404

Open
dima11223432 wants to merge 11 commits into
gofiber:mainfrom
dima11223432:fix/cancellation-chain
Open

🔥 feat: added cancelFunc and async goroutine to monitore lost of connec…#4404
dima11223432 wants to merge 11 commits into
gofiber:mainfrom
dima11223432:fix/cancellation-chain

Conversation

@dima11223432

Copy link
Copy Markdown

Description

This PR fixes a critical resource leak and testing issue where the background monitoring goroutine for client-disconnect detection could get stuck in a select block indefinitely.

When a middleware (like timeout) abandons the original request context or a context panics after a timeout, the fasthttp.RequestCtx might never close its connection channel in test environments.

The solution ensures the lifecycle of the connection-monitoring goroutine is strictly bound to the local request context (reqCtx.Done()), allowing it to gracefully exit the moment the context is canceled or reset by the Fiber engine.

Fixes #4338
Changes introduced

Robust Goroutine Lifecycle: Updated the connection-monitoring go func() inside DefaultCtx.Reset to actively listen to reqCtx.Done().

Prevented Memory Leaks: Guaranteed that resetting the context or releasing it back to the pool (release()) immediately shuts down any pending background monitoring.

Flawless Test Execution: Fixed the pseudo-FAIL state in middleware/timeout tests caused by asynchronous post-timeout panics colliding with the test runner's cleanup phase.

Type of change

[x] Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

[x] Conducted a self-review of the code and provided comments for complex or critical parts.

[x] Added or updated unit tests to validate the effectiveness of the changes or new features.

[x] Ensured that new and existing unit tests pass locally with the changes.

@dima11223432 dima11223432 requested a review from a team as a code owner June 5, 2026 12:39
@welcome

welcome Bot commented Jun 5, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@ReneWerner87 ReneWerner87 added this to v3 Jun 5, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Too much diff to scan? Review this PR in Change Stack to start with the highest-impact changes.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

DefaultCtx gains a cancelFunc field. Context() reads or lazily creates a cancellable request context in fasthttp user values and may spawn a goroutine to cancel it when the connection or context finishes. Reset() and release() cancel and clear stored cancel functions. Tests exercise lazy init, disconnect cancellation, and timeout integration.

Changes

Request-scoped context cancellation

Layer / File(s) Summary
Add cancelFunc field and Context() behavior
ctx.go
DefaultCtx adds a cancelFunc field. Context() reads a context.Context from fasthttp user values (creates one with context.WithCancel if missing), stores it via SetContext, and may spawn a goroutine that cancels the stored context when fasthttp.Done() or the stored context’s Done() fires.
Reset() creates/stores new user context
ctx.go
Reset() cancels and clears any previously stored cancelFunc. When fctx is non-nil, Reset() creates a new cancellable background context, stores it in fasthttp user values under the user context key, marks the user context as set, and records the new cancel function in c.cancelFunc.
release() lifecycle cleanup
ctx.go
release() cancels and clears any stored cancelFunc before performing request cleanup, including clearing fasthttp user context.
Tests: lazy init, cancel-on-disconnect, timeout integration
ctx_test.go, middleware/timeout/timeout_test.go
Adds Test_Ctx_Context_Lazy_Initialization_Suite and Test_Ctx_Context_Cancel_On_Disconnect covering lazy Context() initialization and disconnect-triggered cancellation; adds TestTimeout_Integration_WithLazyContext and TestTimeout_Integration_PanicAfterTimeoutWithLazyCtx to validate timeout middleware behavior with lazy contexts and reclamation after panics.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DefaultCtx as DefaultCtx.Context()
  participant Fasthttp as fasthttp.RequestCtx
  participant Canceller as CancellationGoroutine
  Client->>DefaultCtx: call Context()
  DefaultCtx->>Fasthttp: read user context (user values)
  alt no context
    DefaultCtx->>DefaultCtx: context.WithCancel(background)
    DefaultCtx->>Fasthttp: SetContext(newCtx)
  end
  DefaultCtx->>Canceller: spawn goroutine (if connection present)
  Canceller->>Fasthttp: wait on Fasthttp.Done()
  Canceller->>DefaultCtx: wait on stored ctx.Done()
  alt fasthttp done first
    Canceller->>DefaultCtx: call cancelFunc()
    Canceller->>DefaultCtx: clear cancelFunc
  else ctx done first
    Canceller->>Canceller: return
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • gaby
  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 I nibble at deadlines, cancel with grace,
a tiny goroutine guards each request's space.
Lazy sprouts grow when handlers call,
then Reset clears crumbs and tidies all.
Hop, tidy, sleep — contexts safe in place.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description adequately covers the problem, solution, and impact; however, required checklist items for documentation updates and benchmarks are not addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: adding cancelFunc handling and async disconnect monitoring in the context lifecycle.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces context cancellation handling in DefaultCtx by adding a cancelFunc field, managing context cancellation inside Context(), and resetting/canceling it during Reset(). However, the reviewer identified critical issues with these changes: the background goroutine and 10ms ticker introduced in Reset() cause severe performance regressions, data races, and goroutine/CPU leaks. Additionally, the background goroutine in Context() introduces a potential deadlock, goroutine leak, and data race. The reviewer recommends removing the background goroutine from Reset() entirely and provides a refactored implementation for Context() to safely manage the context lifecycle.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread ctx.go
Comment thread ctx.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ctx.go`:
- Around line 717-738: The watcher goroutine in DefaultCtx.Reset reads
c.fasthttp concurrently, causing a race and potential nil deref; change the
goroutine to capture the per-request *fasthttp.RequestCtx into a local (e.g.
fctx := c.fasthttp) and use fctx.UserValue(userContextKey) instead of reading
c.fasthttp on each tick, and modify release() to call and nil c.cancelFunc
(invoke the stored cancel function and set c.cancelFunc = nil) so the ticker
goroutine is deterministically stopped when the context is returned to the pool.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c158d94b-b512-48cf-8b19-a09d3b07ee5d

📥 Commits

Reviewing files that changed from the base of the PR and between 5fe6c52 and 20dddea.

📒 Files selected for processing (1)
  • ctx.go

Comment thread ctx.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ctx.go (1)

143-155: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Multiple goroutines spawned per request and race condition on cancelFunc.

Two issues with the goroutine spawn logic:

  1. Multiple goroutines per request: The goroutine spawn block runs on every Context() call when c.cancelFunc != nil. Since cancelFunc is set in Reset() and never cleared after spawning, each subsequent call to Context() spawns another goroutine doing the same work.

  2. Race on cancelFunc capture: Between the check c.cancelFunc != nil (line 143) and the capture cancel := c.cancelFunc (line 146), another goroutine could call Reset()/release() setting c.cancelFunc = nil. The captured cancel becomes nil, causing a panic when the goroutine calls cancel().

🐛 Suggested fix: atomically claim cancelFunc when spawning
-	if c.cancelFunc != nil && c.fasthttp.Conn() != nil {
-		fastHttpDone := c.fasthttp.Done()
-		reqCtx := ctx
-		cancel := c.cancelFunc
-
-		go func() {
-			select {
-			case <-fastHttpDone:
-				cancel()
-			case <-reqCtx.Done():
-			}
-		}()
-	}
+	// Atomically claim the cancelFunc to spawn the monitoring goroutine only once.
+	// Once claimed (set to nil here), subsequent Context() calls won't spawn duplicates.
+	if cancel := c.cancelFunc; cancel != nil && c.fasthttp.Conn() != nil {
+		c.cancelFunc = nil // Claim: prevent duplicate goroutines
+		fastHttpDone := c.fasthttp.Done()
+		reqCtx := ctx
+
+		go func() {
+			select {
+			case <-fastHttpDone:
+				cancel()
+			case <-reqCtx.Done():
+			}
+		}()
+	}

Note: If cancelFunc is cleared in Context(), ensure Reset() and release() still check for nil before calling, which they already do. However, this means calling c.cancelFunc() in Reset()/release() may not cancel an in-flight monitoring goroutine if Context() already claimed it. Consider whether the goroutine's cancel should still be reachable for explicit cleanup, or if relying on reqCtx.Done() is sufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ctx.go` around lines 143 - 155, The monitoring goroutine in Context() can be
spawned repeatedly and races on c.cancelFunc; fix by atomically claiming and
clearing the cancel function before starting the goroutine so only one monitor
is created and you never capture a nil cancel. Modify the Context() logic
(referencing Context(), c.cancelFunc, c.fasthttp.Done()) to acquire the cancel
function under the same synchronization used by Reset()/release() (or use an
atomic compare-and-swap on a pointer to the cancel func) and set c.cancelFunc =
nil as you claim it, then spawn the goroutine with the claimed non-nil cancel;
leave Reset()/release() defensive (check for nil) as they already do.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ctx.go`:
- Around line 143-155: The monitoring goroutine in Context() can be spawned
repeatedly and races on c.cancelFunc; fix by atomically claiming and clearing
the cancel function before starting the goroutine so only one monitor is created
and you never capture a nil cancel. Modify the Context() logic (referencing
Context(), c.cancelFunc, c.fasthttp.Done()) to acquire the cancel function under
the same synchronization used by Reset()/release() (or use an atomic
compare-and-swap on a pointer to the cancel func) and set c.cancelFunc = nil as
you claim it, then spawn the goroutine with the claimed non-nil cancel; leave
Reset()/release() defensive (check for nil) as they already do.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8346da39-823c-4ada-8c67-2c31131f2e97

📥 Commits

Reviewing files that changed from the base of the PR and between 20dddea and 4e77e71.

📒 Files selected for processing (1)
  • ctx.go

@gaby

gaby commented Jun 6, 2026

Copy link
Copy Markdown
Member

@dima11223432 Please add unit-tests/docs before submitting a PR.

@gaby gaby closed this Jun 6, 2026
@github-project-automation github-project-automation Bot moved this to Done in v3 Jun 6, 2026
@dima11223432

Copy link
Copy Markdown
Author

Can i write only unit tests? :)

@ReneWerner87

Copy link
Copy Markdown
Member

Please test your code locally first and add unit tests to it before creating pull requests marked as "done"
We can reopen it ,when the pr is in a clean tested state

@dima11223432

Copy link
Copy Markdown
Author

@ReneWerner87 I have written tests for cancelFunc! Pls, reopen pr

@ReneWerner87 ReneWerner87 reopened this Jun 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ctx_test.go`:
- Around line 9601-9711: Add t.Parallel() at the start of the top-level tests
and each subtest to comply with the repository rule: insert t.Parallel() as the
first statement in Test_Ctx_Context_Lazy_Initialization_Suite, inside each t.Run
callback ("Lazy_Initialization_With_Conn", "Lazy_Initialization_Without_Conn",
"Release_Without_Context_Access"), and at the start of
Test_Ctx_Context_Cancel_On_Disconnect so the tests and subtests run in parallel.
- Around line 9680-9708: Test_Ctx_Context_Cancel_On_Disconnect is cancelling the
injected netCtx directly instead of exercising the disconnect path in
DefaultCtx.Context(); instead of calling cancelNet(), arrange for the fasthttp
Done signal to fire so the code path that checks c.cancelFunc &&
c.fasthttp.Conn() is exercised: create a fasthttp.RequestCtx (or a small test
double) whose Done() returns a cancellable channel (e.g., create a
context.WithCancel and wire its Done into the RequestCtx/Done return), assign
that RequestCtx to c.fasthttp and ensure c.cancelFunc is non-nil (via
c.SetContext or by setting cancelFunc like in production), then trigger the
RequestCtx cancellation (call the cancel returned by context.WithCancel) and
assert goCtx.Done() is closed and goCtx.Err() is context.Canceled rather than
cancelling netCtx directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c7d6b547-d1e4-4818-958d-5446b93968d4

📥 Commits

Reviewing files that changed from the base of the PR and between 03f1e0b and f55f5c4.

📒 Files selected for processing (2)
  • ctx_test.go
  • middleware/timeout/timeout_test.go

Comment thread ctx_test.go
Comment on lines +9601 to +9711
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")
}

app.ReleaseCtx(c)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add t.Parallel() to the new tests/subtests.

The newly added tests/subtests are missing t.Parallel(), so they do not follow this repo’s Go test rule.

♻️ Proposed fix
 func Test_Ctx_Context_Lazy_Initialization_Suite(t *testing.T) {
+	t.Parallel()
 	app := New()

 	// Scenario with fake network connection
 	t.Run("Lazy_Initialization_With_Conn", func(t *testing.T) {
+		t.Parallel()
 		fctx := &fasthttp.RequestCtx{}
 		remoteAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}
 		fctx.Init(&fasthttp.Request{}, remoteAddr, nil)
@@
 	//Scenario without fake network connection
 	t.Run("Lazy_Initialization_Without_Conn", func(t *testing.T) {
+		t.Parallel()
 		ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
@@
 	//Scenario without .Context()
 	t.Run("Release_Without_Context_Access", func(t *testing.T) {
+		t.Parallel()
 		ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
@@
 func Test_Ctx_Context_Cancel_On_Disconnect(t *testing.T) {
+	t.Parallel()
 	app := New()

As per coding guidelines, **/*_test.go: "Always invoke t.Parallel() at the start of each test and subtest to maximize concurrency in Go tests."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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")
}
app.ReleaseCtx(c)
}
func Test_Ctx_Context_Lazy_Initialization_Suite(t *testing.T) {
t.Parallel()
app := New()
// Scenario with fake network connection
t.Run("Lazy_Initialization_With_Conn", func(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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")
}
app.ReleaseCtx(c)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ctx_test.go` around lines 9601 - 9711, Add t.Parallel() at the start of the
top-level tests and each subtest to comply with the repository rule: insert
t.Parallel() as the first statement in
Test_Ctx_Context_Lazy_Initialization_Suite, inside each t.Run callback
("Lazy_Initialization_With_Conn", "Lazy_Initialization_Without_Conn",
"Release_Without_Context_Access"), and at the start of
Test_Ctx_Context_Cancel_On_Disconnect so the tests and subtests run in parallel.

Source: Coding guidelines

Comment thread ctx_test.go
Comment on lines +9680 to +9708
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")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant test (with line numbers)
sed -n '9600,9740p' ctx_test.go | cat -n

# Find the test definition (in case it spans more than the snippet)
rg -n "Test_Ctx_Context_Cancel_On_Disconnect" -S .

# Look for any disconnect / Done / cancellation plumbing in ctx implementation
# (best-effort: locate DefaultCtx.Context / SetContext / disconnect monitor code)
rg -n "type DefaultCtx|func \\(.*DefaultCtx.*\\) Context\\(|SetContext\\(|AcquireCtx\\(" -S . | head -n 50
rg -n "disconnect|Disconnect|fasthttp\\.Done|Done\\(\\)|onDisconnect|net\\." -S . | head -n 80

# Search for other tests asserting disconnect-driven cancellation for comparison
rg -n "Cancel_On_Disconnect|disconnect.*cancel|fasthttp.*cancel|Done.*context" -S .

Repository: gofiber/fiber

Length of output: 16141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact test function (with more surrounding lines for context)
sed -n '9660,9735p' ctx_test.go | cat -n

# Inspect DefaultCtx SetContext / Context / cancellation plumbing
rg -n "func \\(c \\*DefaultCtx\\) (SetContext|Context)\\(" ctx.go
rg -n "cancelFunc|handlerDone|ScheduleReclaim|fasthttp\\.Done|client disconnect|disconnect" ctx.go

# Print relevant sections around those hits (keep it bounded)
python3 - <<'PY'
import re, subprocess, os, textwrap, sys, json, pathlib
# Find line numbers for key functions in ctx.go
targets = ["func (c *DefaultCtx) SetContext(", "func (c *DefaultCtx) Context(", "ScheduleReclaim", "cancelFunc"]
for t in targets:
    out = subprocess.check_output(["bash","-lc", f"rg -n \"{re.escape(t)}\" ctx.go | head -n 5"], text=True)
    if out.strip():
        print(f"\n# HIT: {t}\n{out.strip()}")
PY

# Dump the likely surrounding code blocks in ctx.go for manual review
# (use sed with line ranges after locating exact line numbers)
lines=$(rg -n "func \\(c \\*DefaultCtx\\) (SetContext|Context)\\(" ctx.go | awk -F: '{print $1}' | sort -n | uniq)
for L in $lines; do
  start=$((L-25)); if [ $start -lt 1 ]; then start=1; fi
  end=$((L+120))
  echo -e "\n### ctx.go around line $L"
  sed -n "${start},${end}p" ctx.go | cat -n
done

# Also inspect ScheduleReclaim (where canceling is likely wired)
sch=$(rg -n "func \\(c \\*DefaultCtx\\) ScheduleReclaim\\(" ctx.go | awk -F: '{print $1}' | head -n 1)
if [ -n "$sch" ]; then
  L=$(echo "$sch" | awk -F: '{print $1}')
  start=$((L-40)); if [ $start -lt 1 ]; then start=1; fi
  end=$((L+120))
  echo -e "\n### ctx.go around ScheduleReclaim at line $L"
  sed -n "${start},${end}p" ctx.go | cat -n
fi

# Search in request lifecycle / listener code for disconnect-driven cancellation
rg -n "AcquireCtx\\(|ReleaseCtx\\(|handlerDone|ScheduleReclaim|cancel\\(" -S . | head -n 80

Repository: gofiber/fiber

Length of output: 27653


Test_Ctx_Context_Cancel_On_Disconnect is cancelling the injected context directly, not disconnect

  • The test sets netCtx via c.SetContext(netCtx), and c.Context() returns that context; calling cancelNet() directly cancels goCtx.Done().
  • The disconnect-driven cancellation in DefaultCtx.Context() (cancelling on c.fasthttp.Done() when c.cancelFunc != nil && c.fasthttp.Conn() != nil) is not exercised with the empty fasthttp.RequestCtx{} used here.

Drive cancellation through the transport/disconnect path (or trigger the fasthttp.Done() signal) and assert goCtx.Done() without directly cancelling netCtx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ctx_test.go` around lines 9680 - 9708, Test_Ctx_Context_Cancel_On_Disconnect
is cancelling the injected netCtx directly instead of exercising the disconnect
path in DefaultCtx.Context(); instead of calling cancelNet(), arrange for the
fasthttp Done signal to fire so the code path that checks c.cancelFunc &&
c.fasthttp.Conn() is exercised: create a fasthttp.RequestCtx (or a small test
double) whose Done() returns a cancellable channel (e.g., create a
context.WithCancel and wire its Done into the RequestCtx/Done return), assign
that RequestCtx to c.fasthttp and ensure c.cancelFunc is non-nil (via
c.SetContext or by setting cancelFunc like in production), then trigger the
RequestCtx cancellation (call the cancel returned by context.WithCancel) and
assert goCtx.Done() is closed and goCtx.Err() is context.Canceled rather than
cancelling netCtx directly.

@ReneWerner87

ReneWerner87 commented Jun 6, 2026

Copy link
Copy Markdown
Member

Tests and lint are falling

@dima11223432

Copy link
Copy Markdown
Author

I have resolved linter errors and data race

@gaby gaby changed the title feat: added cancelFunc and async goroutine to monitore lost of connec… 🔥 feat: added cancelFunc and async goroutine to monitore lost of connec… Jun 27, 2026
@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.37037% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.95%. Comparing base (464387b) to head (560918e).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
ctx.go 70.37% 6 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4404      +/-   ##
==========================================
- Coverage   92.02%   91.95%   -0.07%     
==========================================
  Files         138      138              
  Lines       13486    13509      +23     
==========================================
+ Hits        12410    12422      +12     
- Misses        682      691       +9     
- Partials      394      396       +2     
Flag Coverage Δ
unittests 91.95% <70.37%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

🐛 [Bug]: Context.Background() used as default in request path — breaks cancellation chain

3 participants