Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
52ad9e1
🧹 chore: Remove unnecessary blank line in timeout.go
edvardsanta Aug 12, 2025
2ae9cf7
feat: Add safe timeout handling in callOnTimeoutSafe function
edvardsanta Aug 12, 2025
67352ad
feat: Replace direct timeout handling with callOnTimeoutSafe for impr…
edvardsanta Aug 12, 2025
d551813
fix: Change error return type in New function for better error handling
edvardsanta Aug 12, 2025
e57dabe
fix: Enhance timeout handling to capture panics and improve error rep…
edvardsanta Aug 12, 2025
546d89e
test: Add edge case tests for timeout middleware handling
edvardsanta Aug 12, 2025
f636275
fix: Update New function to return error type and handle panics in ti…
edvardsanta Aug 14, 2025
f3a11eb
fix: Update timeout handler signatures to ignore context parameter fo…
edvardsanta Aug 14, 2025
819ce43
Merge branch 'main' into fix/TimeoutMiddlewareNotEnforced
edvardsanta Aug 14, 2025
a1c86f9
🩹 Fix: Improve timeout handling and error reporting in middleware
edvardsanta Sep 18, 2025
cc705c3
Enable parallel execution for timeout middleware tests
edvardsanta Sep 18, 2025
107b4c8
Merge branch 'main' into fix/TimeoutMiddlewareNotEnforced
edvardsanta Sep 18, 2025
6eda14c
🩹 Fix: Remove unnecessary timeout context handling in middleware
edvardsanta Sep 18, 2025
19391e7
🧹 chore: Remove unused import in timeout test file
edvardsanta Sep 18, 2025
30f9c08
♻️ Refactor: Simplify timeout handling in middleware by removing safe…
edvardsanta Sep 18, 2025
4a0452d
♻️ Refactor: Enhance timeout handling in middleware by consolidating …
edvardsanta Sep 18, 2025
c8bea25
🚨 Test: Add parallel tests for timeout middleware and enhance timeout…
edvardsanta Sep 18, 2025
073ed97
Merge branch 'refs/heads/main' into fix/TimeoutMiddlewareNotEnforced
edvardsanta Nov 22, 2025
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
60 changes: 45 additions & 15 deletions middleware/timeout/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,60 @@ func New(h fiber.Handler, config ...Config) fiber.Handler {
ctx.SetContext(parent)
}()

err := runHandler(ctx, h, cfg)
return runHandler(ctx, h, cfg)
}
}

if errors.Is(tCtx.Err(), context.DeadlineExceeded) && err == nil {
if cfg.OnTimeout != nil {
return cfg.OnTimeout(ctx)
func safeCall(fn func() error) error {
err := error(nil)
func() {
defer func() {
if r := recover(); r != nil {
err = fiber.ErrRequestTimeout
}
return fiber.ErrRequestTimeout
}
return err
}
}()
err = fn()
}()
return err
}

// runHandler executes the handler and returns fiber.ErrRequestTimeout if it
// sees a deadline exceeded error or one of the custom "timeout-like" errors.
func runHandler(c fiber.Ctx, h fiber.Handler, cfg Config) error {
err := h(c)
if err != nil && (errors.Is(err, context.DeadlineExceeded) || (len(cfg.Errors) > 0 && isCustomError(err, cfg.Errors))) {
if cfg.OnTimeout != nil {
if toErr := cfg.OnTimeout(c); toErr != nil {
return toErr
done := make(chan error, 1)
panicChan := make(chan any, 1)

go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
done <- h(c)
}()

err := safeCall(func() error {
select {
case err := <-done:
if err != nil && (errors.Is(err, context.DeadlineExceeded) || (len(cfg.Errors) > 0 && isCustomError(err, cfg.Errors))) {
if cfg.OnTimeout != nil {
if toErr := cfg.OnTimeout(c); toErr != nil {
return toErr
}
}
return fiber.ErrRequestTimeout
}
return err
case <-panicChan:
return fiber.ErrRequestTimeout
case <-c.Context().Done():
if cfg.OnTimeout != nil {
return cfg.OnTimeout(c)
}
return fiber.ErrRequestTimeout
}
return fiber.ErrRequestTimeout
}
})

return err
}

Expand Down
108 changes: 108 additions & 0 deletions middleware/timeout/timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,111 @@
require.True(t, called)
require.EqualError(t, err, "handled")
}

// TestTimeout_Issue_3671 tests various edge cases for the timeout middleware.
func TestTimeout_Issue_3671(t *testing.T) {
t.Parallel()
app := fiber.New()
testCases := []struct {
name string
path string
handler fiber.Handler
config Config
expectCode int
}{
{
name: "Handler panics after timeout",
path: "/panic-after-timeout",
handler: func(_ fiber.Ctx) error {
time.Sleep(50 * time.Millisecond)
panic("panic after timeout")
},
config: Config{Timeout: 10 * time.Millisecond},
expectCode: fiber.StatusRequestTimeout,
},
{
name: "Handler blocks forever",
path: "/block-forever",
handler: func(_ fiber.Ctx) error {
select {} // Block forever
},
config: Config{Timeout: 10 * time.Millisecond},
expectCode: fiber.StatusRequestTimeout,
},
{
name: "Timeout set to 1 nanosecond",
path: "/nano",
handler: func(c fiber.Ctx) error {
time.Sleep(1 * time.Millisecond)
return c.SendString("late")
},
config: Config{Timeout: 1 * time.Nanosecond},
expectCode: fiber.StatusRequestTimeout,
},
{
name: "Timeout set to a very large value",
path: "/maxint",
handler: func(c fiber.Ctx) error {
return c.SendString("ok")
},
config: Config{Timeout: 1<<63 - 1},
expectCode: fiber.StatusOK,
},
{
name: "Custom OnTimeout handler panics",
path: "/panic-ontimeout",
handler: func(_ fiber.Ctx) error {
time.Sleep(50 * time.Millisecond)
return nil
},
config: Config{
Timeout: 10 * time.Millisecond,
OnTimeout: func(_ fiber.Ctx) error {
panic("custom panic on timeout")
},
},
expectCode: fiber.StatusRequestTimeout,
},
{
name: "Custom OnTimeout",
path: "/panic-ontimeout",
handler: func(c fiber.Ctx) error {
if err := sleepWithContext(c.Context(), 100*time.Millisecond, context.DeadlineExceeded); err != nil {
return err
}
return c.SendString("should not reach")
},
config: Config{
Timeout: 20 * time.Millisecond,
OnTimeout: func(c fiber.Ctx) error {
return c.Status(408).JSON(fiber.Map{"error": "timeout"})
},
},
expectCode: fiber.StatusRequestTimeout,
},
}

for _, tc := range testCases {
app.Get(tc.path, New(tc.handler, tc.config))
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(fiber.MethodGet, tc.path, nil)

Check failure on line 305 in middleware/timeout/timeout_test.go

View workflow job for this annotation

GitHub Actions / lint

httpNoBody: http.NoBody should be preferred to the nil request body (gocritic)
resp, err := app.Test(req)

require.NoError(t, err)
require.Equal(t, tc.expectCode, resp.StatusCode)
})
}
}

func TestSafeCall_Panic(t *testing.T) {
t.Parallel()
err := safeCall(func() error {
panic("test panic")
})

require.Equal(t, fiber.ErrRequestTimeout, err)
}
Loading