Skip to content

Commit bb7d6d8

Browse files
authored
Merge pull request #4009 from gofiber/fix/timeout-immediate-return-and-context-propagation
2 parents 7104ad1 + 8f0ce51 commit bb7d6d8

File tree

9 files changed

+574
-60
lines changed

9 files changed

+574
-60
lines changed

ctx.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"mime/multipart"
1414
"strconv"
1515
"strings"
16+
"sync/atomic"
1617
"time"
1718

1819
"github.com/gofiber/utils/v2"
@@ -59,8 +60,8 @@ type DefaultCtx struct {
5960
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
6061
bind *Bind // Default bind reference
6162
redirect *Redirect // Default redirect reference
62-
values [maxParams]string // Route parameter values
6363
viewBindMap Map // Default view map to bind template engine
64+
values [maxParams]string // Route parameter values
6465
baseURI string // HTTP base uri
6566
pathOriginal string // Original HTTP path
6667
flashMessages redirectionMsgs // Flash messages
@@ -70,6 +71,7 @@ type DefaultCtx struct {
7071
indexRoute int // Index of the current route
7172
indexHandler int // Index of the current handler
7273
methodInt int // HTTP method INT equivalent
74+
abandoned atomic.Bool // If true, ctx won't be pooled until ForceRelease is called
7375
matched bool // Non use route matched
7476
skipNonUseRoutes bool // Skip non-use routes while iterating middleware
7577
}
@@ -661,7 +663,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {
661663
c.fasthttp.SetUserValue(userContextKey, nil)
662664
}
663665

664-
// Release is a method to reset context fields when to use ReleaseCtx()
666+
// release is a method to reset context fields when to use ReleaseCtx()
665667
func (c *DefaultCtx) release() {
666668
c.route = nil
667669
c.fasthttp = nil
@@ -677,11 +679,41 @@ func (c *DefaultCtx) release() {
677679
c.redirect = nil
678680
}
679681
c.skipNonUseRoutes = false
682+
// performance: no need for using c.abandoned.Store(false) here, as it is always set to false when it was true in ForceRelease
680683
c.handlerCtx = nil
681684
c.DefaultReq.release()
682685
c.DefaultRes.release()
683686
}
684687

688+
// Abandon marks this context as abandoned. An abandoned context will not be
689+
// returned to the pool when ReleaseCtx is called.
690+
//
691+
// This is used by the timeout middleware to return immediately while the
692+
// handler goroutine continues using the context safely.
693+
//
694+
// Only call ForceRelease after Abandon if you can guarantee no other goroutine
695+
// (including Fiber's requestHandler and ErrorHandler) will touch the context.
696+
// The timeout middleware intentionally does NOT call ForceRelease to avoid
697+
// races, which means timed-out requests leak their contexts until a safe
698+
// reclamation strategy exists.
699+
func (c *DefaultCtx) Abandon() {
700+
c.abandoned.Store(true)
701+
}
702+
703+
// IsAbandoned returns true if Abandon() was called on this context.
704+
func (c *DefaultCtx) IsAbandoned() bool {
705+
return c.abandoned.Load()
706+
}
707+
708+
// ForceRelease releases an abandoned context back to the pool.
709+
// This MUST only be called after all goroutines (including requestHandler and
710+
// ErrorHandler) have completely finished using this context. Calling it while
711+
// any goroutine is still running causes races.
712+
func (c *DefaultCtx) ForceRelease() {
713+
c.abandoned.Store(false)
714+
c.app.ReleaseCtx(c)
715+
}
716+
685717
func (c *DefaultCtx) renderExtensions(bind any) {
686718
if bindMap, ok := bind.(Map); ok {
687719
// Bind view map

ctx_interface.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ type CustomCtx interface {
1616
// Reset is a method to reset context fields by given request when to use server handlers.
1717
Reset(fctx *fasthttp.RequestCtx)
1818

19+
// release is called before returning the context to the pool.
20+
release()
21+
22+
// Abandon marks the context as abandoned. An abandoned context will not be
23+
// returned to the pool when ReleaseCtx is called. This is used by the timeout
24+
// middleware to return immediately while the handler goroutine continues.
25+
// The cleanup goroutine must call ForceRelease when the handler finishes.
26+
Abandon()
27+
28+
// IsAbandoned returns true if the context has been abandoned.
29+
IsAbandoned() bool
30+
31+
// ForceRelease releases an abandoned context back to the pool.
32+
// Must only be called after the handler goroutine has completely finished.
33+
ForceRelease()
34+
1935
// Methods to use with next stack.
2036
getMethodInt() int
2137
getIndexRoute() int
@@ -66,7 +82,14 @@ func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) CustomCtx {
6682
}
6783

6884
// ReleaseCtx releases the ctx back into the pool.
85+
// If the context was abandoned (e.g., by timeout middleware), this is a no-op.
86+
// Call ForceRelease only when you can guarantee no goroutines (including the
87+
// requestHandler and ErrorHandler) still touch the context; the timeout
88+
// middleware intentionally leaves abandoned contexts unreleased to avoid races.
6989
func (app *App) ReleaseCtx(c CustomCtx) {
90+
if c.IsAbandoned() {
91+
return
92+
}
7093
c.release()
7194
app.pool.Put(c)
7295
}

ctx_interface_gen.go

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ctx_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7956,6 +7956,44 @@ func Test_Ctx_OverrideParam(t *testing.T) {
79567956
})
79577957
}
79587958

7959+
func Test_Ctx_AbandonSkipsReleaseCtx(t *testing.T) {
7960+
t.Parallel()
7961+
7962+
app := New()
7963+
ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // controlled test setup
7964+
ctx.route = &Route{}
7965+
7966+
t.Cleanup(func() {
7967+
ctx.ForceRelease()
7968+
})
7969+
7970+
require.False(t, ctx.IsAbandoned())
7971+
7972+
ctx.Abandon()
7973+
require.True(t, ctx.IsAbandoned())
7974+
7975+
app.ReleaseCtx(ctx)
7976+
7977+
require.True(t, ctx.IsAbandoned(), "ReleaseCtx must not pool abandoned contexts")
7978+
require.NotNil(t, ctx.fasthttp, "ReleaseCtx should not reset fasthttp on abandoned ctx")
7979+
require.NotNil(t, ctx.route, "ReleaseCtx should not reset route on abandoned ctx")
7980+
}
7981+
7982+
func Test_Ctx_ForceReleaseClearsAbandon(t *testing.T) {
7983+
t.Parallel()
7984+
7985+
app := New()
7986+
ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // controlled test setup
7987+
ctx.route = &Route{}
7988+
7989+
ctx.Abandon()
7990+
ctx.ForceRelease()
7991+
7992+
require.False(t, ctx.IsAbandoned(), "ForceRelease should clear abandon flag")
7993+
require.Nil(t, ctx.fasthttp, "ForceRelease should release fasthttp reference")
7994+
require.Nil(t, ctx.route, "ForceRelease should reset route before pooling")
7995+
}
7996+
79597997
// go test -v -run=^$ -bench=Benchmark_Ctx_IsProxyTrusted -benchmem -count=4
79607998
func Benchmark_Ctx_IsProxyTrusted(b *testing.B) {
79617999
// Scenario without trusted proxy check

docs/api/ctx.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ description: >-
88
sidebar_position: 3
99
---
1010

11+
### Abandon
12+
13+
Marks the context as abandoned. An abandoned context will not be returned to the pool when `ReleaseCtx` is called. This is used internally by the [timeout middleware](../middleware/timeout.md) to return immediately while the handler goroutine continues safely.
14+
15+
```go title="Signature"
16+
func (c fiber.Ctx) Abandon()
17+
func (c fiber.Ctx) IsAbandoned() bool
18+
func (c fiber.Ctx) ForceRelease()
19+
```
20+
21+
| Method | Description |
22+
|:---------------|:----------------------------------------------------------------------------|
23+
| `Abandon()` | Marks the context as abandoned. ReleaseCtx becomes a no-op for this context. |
24+
| `IsAbandoned()`| Returns `true` if `Abandon()` was called on this context. |
25+
| `ForceRelease()`| Releases an abandoned context back to the pool. Must only be called after the handler has completely finished. |
26+
27+
:::caution
28+
These methods are primarily for internal use and advanced middleware development. Most applications should not need to call them directly.
29+
:::
30+
1131
### App
1232

1333
Returns the [\*App](app.md) reference so you can easily access all application settings.

docs/middleware/timeout.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,34 @@ id: timeout
44

55
# Timeout
66

7-
The timeout middleware aborts handlers that run too long. It wraps them with
7+
The timeout middleware enforces a deadline on handler execution. It wraps handlers with
88
`context.WithTimeout`, exposes the derived context through `c.Context()`, and
99
returns `408 Request Timeout` when the deadline is exceeded.
1010

11+
## How It Works
12+
13+
When a timeout occurs, the middleware **returns immediately** without waiting for the
14+
handler to finish. This is achieved through Fiber's **Abandon mechanism**:
15+
16+
1. The handler runs in a goroutine with a timeout context
17+
2. On timeout, the middleware marks the context as "abandoned" and returns `408` immediately
18+
3. The handler goroutine can continue safely (e.g., for cleanup) without blocking the response
19+
4. A background cleanup goroutine waits for the handler to finish and performs context cleanup
20+
21+
Handlers can detect the timeout by listening on `c.Context().Done()` and return early.
22+
This is the recommended pattern for cooperative cancellation.
23+
24+
If a handler panics, the middleware catches it and returns `500 Internal Server Error`.
25+
26+
## Known limitations
27+
28+
- Timed-out requests abandon their `fiber.Ctx` to avoid data races with the core
29+
request handler (including the `ErrorHandler`). These contexts are **not**
30+
returned to the pool, so each timed-out request leaks a context. Calling
31+
`ForceRelease` is only safe if you can guarantee that no goroutine (including
32+
Fiber internals) will touch the context anymore; the timeout middleware
33+
intentionally does not call it.
34+
1135
:::caution
1236
`timeout.New` wraps your final handler and can't be added with `app.Use` or
1337
used in a middleware chain. Register it per route and avoid calling
@@ -78,12 +102,12 @@ curl -i http://localhost:3000/sleep/3000 # returns 408 Request Timeout
78102

79103
## Config
80104

81-
| Property | Type | Description | Default |
82-
|:----------|:-------------------|:---------------------------------------------------------------------|:-------|
83-
| Next | `func(fiber.Ctx) bool` | Function to skip this middleware when it returns `true`. | `nil` |
84-
| Timeout | `time.Duration` | Timeout duration for requests. `0` or a negative value disables the timeout. | `0` |
85-
| OnTimeout | `fiber.Handler` | Handler executed when a timeout occurs. Defaults to returning `fiber.ErrRequestTimeout`. | `nil` |
86-
| Errors | `[]error` | Custom errors treated as timeout errors. | `nil` |
105+
| Property | Type | Description | Default |
106+
|:------------|:-------------------|:---------------------------------------------------------------------|:-------|
107+
| Next | `func(fiber.Ctx) bool` | Function to skip this middleware when it returns `true`. | `nil` |
108+
| Timeout | `time.Duration` | Timeout duration for requests. `0` or a negative value disables the timeout. | `0` |
109+
| OnTimeout | `fiber.Handler` | Handler executed when a timeout occurs. Defaults to returning `fiber.ErrRequestTimeout`. | `nil` |
110+
| Errors | `[]error` | Custom errors treated as timeout errors. | `nil` |
87111

88112
### Use with a custom error
89113

@@ -127,11 +151,11 @@ func main() {
127151

128152
handler := func(ctx fiber.Ctx) error {
129153
tran := db.WithContext(ctx.Context()).Begin()
130-
154+
131155
if tran = tran.Exec("SELECT pg_sleep(50)"); tran.Error != nil {
132156
return tran.Error
133157
}
134-
158+
135159
if tran = tran.Commit(); tran.Error != nil {
136160
return tran.Error
137161
}

docs/whats_new.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,19 @@ For more details on these changes and migration instructions, check the [Session
15951595

15961596
The timeout middleware is now configurable. A new `Config` struct allows customizing the timeout duration, defining a handler that runs when a timeout occurs, and specifying errors to treat as timeouts. The `New` function now accepts a `Config` value instead of a duration.
15971597

1598+
**Behavioral changes:**
1599+
1600+
- **Immediate return on timeout**: The middleware now returns immediately when a timeout occurs, without waiting for the handler to finish. This is achieved through the new **Abandon mechanism** which marks the context as abandoned so it won't be returned to the pool while the handler is still running.
1601+
- **Context propagation**: The timeout context is properly propagated to the handler. Handlers can detect timeouts by listening on `c.Context().Done()` and return early.
1602+
- **Panic handling**: Panics in the handler are caught and converted to `500 Internal Server Error` responses.
1603+
- **Race-free design**: The implementation uses fasthttp's `TimeoutErrorWithCode` combined with Fiber's Abandon mechanism to ensure complete race-freedom between the middleware, handler goroutine, and context pooling.
1604+
1605+
**New Ctx methods for the Abandon mechanism:**
1606+
1607+
- `Abandon()`: Marks the context as abandoned
1608+
- `IsAbandoned()`: Returns true if the context was abandoned
1609+
- `ForceRelease()`: Releases an abandoned context back to the pool (for advanced use)
1610+
15981611
**Migration:** Replace calls like `timeout.New(handler, 2*time.Second)` with `timeout.New(handler, timeout.Config{Timeout: 2 * time.Second})`.
15991612

16001613
## 🔌 Addons
@@ -2863,6 +2876,12 @@ app.Use(timeout.New(handler, 2*time.Second))
28632876
app.Use(timeout.New(handler, timeout.Config{Timeout: 2 * time.Second}))
28642877
```
28652878

2879+
**Important behavioral changes:**
2880+
2881+
- The middleware now returns immediately on timeout without waiting for the handler (using the new Abandon mechanism).
2882+
- Handlers can detect timeouts by listening on `c.Context().Done()` and return early.
2883+
- Panics in the handler are caught and converted to `500 Internal Server Error`.
2884+
28662885
#### Filesystem
28672886

28682887
You need to move filesystem middleware to static middleware due to it has been removed from the core.

0 commit comments

Comments
 (0)