mirror of
https://github.com/labstack/echo.git
synced 2025-12-17 23:49:25 +02:00
deprecate timeout middleware
This commit is contained in:
113
CHANGELOG.md
113
CHANGELOG.md
@@ -1,5 +1,118 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v4.15.0 - TBD
|
||||||
|
|
||||||
|
**DEPRECATION NOTICE** Timeout Middleware Deprecated - Use ContextTimeout Instead
|
||||||
|
|
||||||
|
The `middleware.Timeout` middleware has been **deprecated** due to fundamental architectural issues that cause
|
||||||
|
data races. Use `middleware.ContextTimeout` or `middleware.ContextTimeoutWithConfig` instead.
|
||||||
|
|
||||||
|
**Why is this being deprecated?**
|
||||||
|
|
||||||
|
The Timeout middleware manipulates response writers across goroutine boundaries, which causes data races that
|
||||||
|
cannot be reliably fixed without a complete architectural redesign. The middleware:
|
||||||
|
|
||||||
|
- Swaps the response writer using `http.TimeoutHandler`
|
||||||
|
- Must be the first middleware in the chain (fragile constraint)
|
||||||
|
- Can cause races with other middleware (Logger, metrics, custom middleware)
|
||||||
|
- Has been the source of multiple race condition fixes over the years
|
||||||
|
|
||||||
|
**What should you use instead?**
|
||||||
|
|
||||||
|
The `ContextTimeout` middleware (available since v4.12.0) provides timeout functionality using Go's standard
|
||||||
|
context mechanism. It is:
|
||||||
|
|
||||||
|
- Race-free by design
|
||||||
|
- Can be placed anywhere in the middleware chain
|
||||||
|
- Simpler and more maintainable
|
||||||
|
- Compatible with all other middleware
|
||||||
|
|
||||||
|
**Migration Guide:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before (deprecated):
|
||||||
|
e.Use(middleware.Timeout())
|
||||||
|
|
||||||
|
// After (recommended):
|
||||||
|
e.Use(middleware.ContextTimeout(30 * time.Second))
|
||||||
|
```
|
||||||
|
|
||||||
|
With configuration:
|
||||||
|
```go
|
||||||
|
// Before (deprecated):
|
||||||
|
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return c.Path() == "/health"
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// After (recommended):
|
||||||
|
e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return c.Path() == "/health"
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Behavioral Differences:**
|
||||||
|
|
||||||
|
1. **Handler cooperation required**: With ContextTimeout, your handlers must check `context.Done()` for cooperative
|
||||||
|
cancellation. The old Timeout middleware would send a 503 response regardless of handler cooperation, but had
|
||||||
|
data race issues.
|
||||||
|
|
||||||
|
2. **Error handling**: ContextTimeout returns errors through the standard error handling flow. Handlers that receive
|
||||||
|
`context.DeadlineExceeded` should handle it appropriately:
|
||||||
|
|
||||||
|
```go
|
||||||
|
e.GET("/long-task", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
// Example: database query with context
|
||||||
|
result, err := db.QueryContext(ctx, "SELECT * FROM large_table")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
// Handle timeout
|
||||||
|
return echo.NewHTTPError(http.StatusServiceUnavailable, "Request timeout")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, result)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Background tasks**: For long-running background tasks, use goroutines with context:
|
||||||
|
|
||||||
|
```go
|
||||||
|
e.GET("/async-task", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
resultCh := make(chan Result, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result, err := performLongTask(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultCh <- result
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultCh:
|
||||||
|
return c.JSON(http.StatusOK, result)
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return echo.NewHTTPError(http.StatusServiceUnavailable, "Request timeout")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## v4.14.0 - 2025-12-11
|
## v4.14.0 - 2025-12-11
|
||||||
|
|
||||||
`middleware.Logger` has been deprecated. For request logging, use `middleware.RequestLogger` or
|
`middleware.Logger` has been deprecated. For request logging, use `middleware.RequestLogger` or
|
||||||
|
|||||||
@@ -11,6 +11,39 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ContextTimeout Middleware
|
||||||
|
//
|
||||||
|
// ContextTimeout provides request timeout functionality using Go's context mechanism.
|
||||||
|
// It is the recommended replacement for the deprecated Timeout middleware.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Basic Usage:
|
||||||
|
//
|
||||||
|
// e.Use(middleware.ContextTimeout(30 * time.Second))
|
||||||
|
//
|
||||||
|
// With Configuration:
|
||||||
|
//
|
||||||
|
// e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
|
||||||
|
// Timeout: 30 * time.Second,
|
||||||
|
// Skipper: middleware.DefaultSkipper,
|
||||||
|
// }))
|
||||||
|
//
|
||||||
|
// Handler Example:
|
||||||
|
//
|
||||||
|
// e.GET("/task", func(c echo.Context) error {
|
||||||
|
// ctx := c.Request().Context()
|
||||||
|
//
|
||||||
|
// result, err := performTaskWithContext(ctx)
|
||||||
|
// if err != nil {
|
||||||
|
// if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
// return echo.NewHTTPError(http.StatusServiceUnavailable, "timeout")
|
||||||
|
// }
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return c.JSON(http.StatusOK, result)
|
||||||
|
// })
|
||||||
|
|
||||||
// ContextTimeoutConfig defines the config for ContextTimeout middleware.
|
// ContextTimeoutConfig defines the config for ContextTimeout middleware.
|
||||||
type ContextTimeoutConfig struct {
|
type ContextTimeoutConfig struct {
|
||||||
// Skipper defines a function to skip middleware.
|
// Skipper defines a function to skip middleware.
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ import (
|
|||||||
//
|
//
|
||||||
|
|
||||||
// TimeoutConfig defines the config for Timeout middleware.
|
// TimeoutConfig defines the config for Timeout middleware.
|
||||||
|
//
|
||||||
|
// Deprecated: Use ContextTimeoutConfig with ContextTimeout or ContextTimeoutWithConfig instead.
|
||||||
|
// The Timeout middleware has architectural issues that cause data races due to response writer
|
||||||
|
// manipulation across goroutines. It must be the first middleware in the chain, making it fragile.
|
||||||
|
// The ContextTimeout middleware provides timeout functionality using Go's context mechanism,
|
||||||
|
// which is race-free and can be placed anywhere in the middleware chain.
|
||||||
type TimeoutConfig struct {
|
type TimeoutConfig struct {
|
||||||
// Skipper defines a function to skip middleware.
|
// Skipper defines a function to skip middleware.
|
||||||
Skipper Skipper
|
Skipper Skipper
|
||||||
@@ -89,11 +95,38 @@ var DefaultTimeoutConfig = TimeoutConfig{
|
|||||||
|
|
||||||
// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
|
// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
|
||||||
// call runs for longer than its time limit. NB: timeout does not stop handler execution.
|
// call runs for longer than its time limit. NB: timeout does not stop handler execution.
|
||||||
|
//
|
||||||
|
// Deprecated: Use ContextTimeout instead. This middleware has known data race issues due to response writer
|
||||||
|
// manipulation. See https://github.com/labstack/echo/blob/master/middleware/context_timeout.go for the
|
||||||
|
// recommended alternative.
|
||||||
|
//
|
||||||
|
// Example migration:
|
||||||
|
//
|
||||||
|
// // Before:
|
||||||
|
// e.Use(middleware.Timeout())
|
||||||
|
//
|
||||||
|
// // After:
|
||||||
|
// e.Use(middleware.ContextTimeout(30 * time.Second))
|
||||||
func Timeout() echo.MiddlewareFunc {
|
func Timeout() echo.MiddlewareFunc {
|
||||||
return TimeoutWithConfig(DefaultTimeoutConfig)
|
return TimeoutWithConfig(DefaultTimeoutConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeoutWithConfig returns a Timeout middleware with config or panics on invalid configuration.
|
// TimeoutWithConfig returns a Timeout middleware with config or panics on invalid configuration.
|
||||||
|
//
|
||||||
|
// Deprecated: Use ContextTimeoutWithConfig instead. This middleware has architectural data race issues.
|
||||||
|
// See the ContextTimeout middleware for a race-free alternative that uses Go's context mechanism.
|
||||||
|
//
|
||||||
|
// Example migration:
|
||||||
|
//
|
||||||
|
// // Before:
|
||||||
|
// e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||||
|
// Timeout: 30 * time.Second,
|
||||||
|
// }))
|
||||||
|
//
|
||||||
|
// // After:
|
||||||
|
// e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
|
||||||
|
// Timeout: 30 * time.Second,
|
||||||
|
// }))
|
||||||
func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
|
func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
|
||||||
mw, err := config.ToMiddleware()
|
mw, err := config.ToMiddleware()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -103,6 +136,8 @@ func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ToMiddleware converts Config to middleware or returns an error for invalid configuration
|
// ToMiddleware converts Config to middleware or returns an error for invalid configuration
|
||||||
|
//
|
||||||
|
// Deprecated: Use ContextTimeoutConfig.ToMiddleware instead.
|
||||||
func (config TimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
func (config TimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
||||||
if config.Skipper == nil {
|
if config.Skipper == nil {
|
||||||
config.Skipper = DefaultTimeoutConfig.Skipper
|
config.Skipper = DefaultTimeoutConfig.Skipper
|
||||||
|
|||||||
Reference in New Issue
Block a user