1
0
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:
toimtoimtoim
2025-12-12 10:04:55 +02:00
committed by Martti T.
parent c9b8b36c9a
commit cdcf16d3cf
3 changed files with 181 additions and 0 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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