diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4fa25a..28b2652f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,118 @@ # 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 `middleware.Logger` has been deprecated. For request logging, use `middleware.RequestLogger` or diff --git a/middleware/context_timeout.go b/middleware/context_timeout.go index 02bd6d1b..5d9ae975 100644 --- a/middleware/context_timeout.go +++ b/middleware/context_timeout.go @@ -11,6 +11,39 @@ import ( "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. type ContextTimeoutConfig struct { // Skipper defines a function to skip middleware. diff --git a/middleware/timeout.go b/middleware/timeout.go index c2aebef3..c0a77a4b 100644 --- a/middleware/timeout.go +++ b/middleware/timeout.go @@ -59,6 +59,12 @@ import ( // // 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 { // Skipper defines a function to skip middleware. 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 // 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 { return TimeoutWithConfig(DefaultTimeoutConfig) } // 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 { mw, err := config.ToMiddleware() 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 +// +// Deprecated: Use ContextTimeoutConfig.ToMiddleware instead. func (config TimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if config.Skipper == nil { config.Skipper = DefaultTimeoutConfig.Skipper