Skip to content

Commit cdcf16d

Browse files
committed
deprecate timeout middleware
1 parent c9b8b36 commit cdcf16d

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed

CHANGELOG.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,118 @@
11
# Changelog
22

3+
## v4.15.0 - TBD
4+
5+
**DEPRECATION NOTICE** Timeout Middleware Deprecated - Use ContextTimeout Instead
6+
7+
The `middleware.Timeout` middleware has been **deprecated** due to fundamental architectural issues that cause
8+
data races. Use `middleware.ContextTimeout` or `middleware.ContextTimeoutWithConfig` instead.
9+
10+
**Why is this being deprecated?**
11+
12+
The Timeout middleware manipulates response writers across goroutine boundaries, which causes data races that
13+
cannot be reliably fixed without a complete architectural redesign. The middleware:
14+
15+
- Swaps the response writer using `http.TimeoutHandler`
16+
- Must be the first middleware in the chain (fragile constraint)
17+
- Can cause races with other middleware (Logger, metrics, custom middleware)
18+
- Has been the source of multiple race condition fixes over the years
19+
20+
**What should you use instead?**
21+
22+
The `ContextTimeout` middleware (available since v4.12.0) provides timeout functionality using Go's standard
23+
context mechanism. It is:
24+
25+
- Race-free by design
26+
- Can be placed anywhere in the middleware chain
27+
- Simpler and more maintainable
28+
- Compatible with all other middleware
29+
30+
**Migration Guide:**
31+
32+
```go
33+
// Before (deprecated):
34+
e.Use(middleware.Timeout())
35+
36+
// After (recommended):
37+
e.Use(middleware.ContextTimeout(30 * time.Second))
38+
```
39+
40+
With configuration:
41+
```go
42+
// Before (deprecated):
43+
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
44+
Timeout: 30 * time.Second,
45+
Skipper: func(c echo.Context) bool {
46+
return c.Path() == "/health"
47+
},
48+
}))
49+
50+
// After (recommended):
51+
e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
52+
Timeout: 30 * time.Second,
53+
Skipper: func(c echo.Context) bool {
54+
return c.Path() == "/health"
55+
},
56+
}))
57+
```
58+
59+
**Important Behavioral Differences:**
60+
61+
1. **Handler cooperation required**: With ContextTimeout, your handlers must check `context.Done()` for cooperative
62+
cancellation. The old Timeout middleware would send a 503 response regardless of handler cooperation, but had
63+
data race issues.
64+
65+
2. **Error handling**: ContextTimeout returns errors through the standard error handling flow. Handlers that receive
66+
`context.DeadlineExceeded` should handle it appropriately:
67+
68+
```go
69+
e.GET("/long-task", func(c echo.Context) error {
70+
ctx := c.Request().Context()
71+
72+
// Example: database query with context
73+
result, err := db.QueryContext(ctx, "SELECT * FROM large_table")
74+
if err != nil {
75+
if errors.Is(err, context.DeadlineExceeded) {
76+
// Handle timeout
77+
return echo.NewHTTPError(http.StatusServiceUnavailable, "Request timeout")
78+
}
79+
return err
80+
}
81+
82+
return c.JSON(http.StatusOK, result)
83+
})
84+
```
85+
86+
3. **Background tasks**: For long-running background tasks, use goroutines with context:
87+
88+
```go
89+
e.GET("/async-task", func(c echo.Context) error {
90+
ctx := c.Request().Context()
91+
92+
resultCh := make(chan Result, 1)
93+
errCh := make(chan error, 1)
94+
95+
go func() {
96+
result, err := performLongTask(ctx)
97+
if err != nil {
98+
errCh <- err
99+
return
100+
}
101+
resultCh <- result
102+
}()
103+
104+
select {
105+
case result := <-resultCh:
106+
return c.JSON(http.StatusOK, result)
107+
case err := <-errCh:
108+
return err
109+
case <-ctx.Done():
110+
return echo.NewHTTPError(http.StatusServiceUnavailable, "Request timeout")
111+
}
112+
})
113+
```
114+
115+
3116
## v4.14.0 - 2025-12-11
4117

5118
`middleware.Logger` has been deprecated. For request logging, use `middleware.RequestLogger` or

middleware/context_timeout.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,39 @@ import (
1111
"github.com/labstack/echo/v4"
1212
)
1313

14+
// ContextTimeout Middleware
15+
//
16+
// ContextTimeout provides request timeout functionality using Go's context mechanism.
17+
// It is the recommended replacement for the deprecated Timeout middleware.
18+
//
19+
//
20+
// Basic Usage:
21+
//
22+
// e.Use(middleware.ContextTimeout(30 * time.Second))
23+
//
24+
// With Configuration:
25+
//
26+
// e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
27+
// Timeout: 30 * time.Second,
28+
// Skipper: middleware.DefaultSkipper,
29+
// }))
30+
//
31+
// Handler Example:
32+
//
33+
// e.GET("/task", func(c echo.Context) error {
34+
// ctx := c.Request().Context()
35+
//
36+
// result, err := performTaskWithContext(ctx)
37+
// if err != nil {
38+
// if errors.Is(err, context.DeadlineExceeded) {
39+
// return echo.NewHTTPError(http.StatusServiceUnavailable, "timeout")
40+
// }
41+
// return err
42+
// }
43+
//
44+
// return c.JSON(http.StatusOK, result)
45+
// })
46+
1447
// ContextTimeoutConfig defines the config for ContextTimeout middleware.
1548
type ContextTimeoutConfig struct {
1649
// Skipper defines a function to skip middleware.

middleware/timeout.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ import (
5959
//
6060

6161
// TimeoutConfig defines the config for Timeout middleware.
62+
//
63+
// Deprecated: Use ContextTimeoutConfig with ContextTimeout or ContextTimeoutWithConfig instead.
64+
// The Timeout middleware has architectural issues that cause data races due to response writer
65+
// manipulation across goroutines. It must be the first middleware in the chain, making it fragile.
66+
// The ContextTimeout middleware provides timeout functionality using Go's context mechanism,
67+
// which is race-free and can be placed anywhere in the middleware chain.
6268
type TimeoutConfig struct {
6369
// Skipper defines a function to skip middleware.
6470
Skipper Skipper
@@ -89,11 +95,38 @@ var DefaultTimeoutConfig = TimeoutConfig{
8995

9096
// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
9197
// call runs for longer than its time limit. NB: timeout does not stop handler execution.
98+
//
99+
// Deprecated: Use ContextTimeout instead. This middleware has known data race issues due to response writer
100+
// manipulation. See https://github.com/labstack/echo/blob/master/middleware/context_timeout.go for the
101+
// recommended alternative.
102+
//
103+
// Example migration:
104+
//
105+
// // Before:
106+
// e.Use(middleware.Timeout())
107+
//
108+
// // After:
109+
// e.Use(middleware.ContextTimeout(30 * time.Second))
92110
func Timeout() echo.MiddlewareFunc {
93111
return TimeoutWithConfig(DefaultTimeoutConfig)
94112
}
95113

96114
// TimeoutWithConfig returns a Timeout middleware with config or panics on invalid configuration.
115+
//
116+
// Deprecated: Use ContextTimeoutWithConfig instead. This middleware has architectural data race issues.
117+
// See the ContextTimeout middleware for a race-free alternative that uses Go's context mechanism.
118+
//
119+
// Example migration:
120+
//
121+
// // Before:
122+
// e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
123+
// Timeout: 30 * time.Second,
124+
// }))
125+
//
126+
// // After:
127+
// e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
128+
// Timeout: 30 * time.Second,
129+
// }))
97130
func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
98131
mw, err := config.ToMiddleware()
99132
if err != nil {
@@ -103,6 +136,8 @@ func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
103136
}
104137

105138
// ToMiddleware converts Config to middleware or returns an error for invalid configuration
139+
//
140+
// Deprecated: Use ContextTimeoutConfig.ToMiddleware instead.
106141
func (config TimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
107142
if config.Skipper == nil {
108143
config.Skipper = DefaultTimeoutConfig.Skipper

0 commit comments

Comments
 (0)