@@ -12,24 +12,276 @@ import (
1212)
1313
1414// ContextTimeoutConfig defines the config for ContextTimeout middleware.
15+ //
16+ // # Overview
17+ //
18+ // ContextTimeout middleware provides timeout functionality by setting a timeout on the request context.
19+ // This is the RECOMMENDED approach for handling request timeouts in Echo, as opposed to the
20+ // deprecated Timeout middleware which has known issues.
21+ //
22+ // # Key Differences from Timeout Middleware
23+ //
24+ // Unlike the deprecated Timeout middleware, ContextTimeout:
25+ // - Does NOT interfere with the response writer
26+ // - Does NOT cause data races when placed in different middleware positions
27+ // - Relies on handlers to check context.Context.Done() for cooperative cancellation
28+ // - Returns errors instead of writing responses directly
29+ // - Is safe to use in any middleware position
30+ //
31+ // # How It Works
32+ //
33+ // 1. Creates a context.WithTimeout() from the request context
34+ // 2. Sets the timeout context on the request
35+ // 3. Calls the next handler
36+ // 4. If the handler returns context.DeadlineExceeded, converts it to HTTP 503
37+ //
38+ // # Handler Requirements
39+ //
40+ // For ContextTimeout to work effectively, your handlers must:
41+ // - Check ctx.Done() in long-running operations
42+ // - Use context-aware APIs (database queries, HTTP calls, etc.)
43+ // - Return context.DeadlineExceeded when the context is cancelled
44+ //
45+ // # Configuration Examples
46+ //
47+ // ## Basic Usage
48+ //
49+ // e.Use(middleware.ContextTimeout(30 * time.Second))
50+ //
51+ // ## Custom Configuration
52+ //
53+ // e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
54+ // Timeout: 30 * time.Second,
55+ // ErrorHandler: func(err error, c echo.Context) error {
56+ // if errors.Is(err, context.DeadlineExceeded) {
57+ // return c.JSON(http.StatusRequestTimeout, map[string]string{
58+ // "error": "Request took too long to process",
59+ // })
60+ // }
61+ // return err
62+ // },
63+ // }))
64+ //
65+ // ## Skip Certain Routes
66+ //
67+ // e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
68+ // Timeout: 30 * time.Second,
69+ // Skipper: func(c echo.Context) bool {
70+ // // Skip timeout for health check endpoints
71+ // return c.Request().URL.Path == "/health"
72+ // },
73+ // }))
74+ //
75+ // # Handler Examples
76+ //
77+ // ## Context-Aware Database Query
78+ //
79+ // e.GET("/users", func(c echo.Context) error {
80+ // ctx := c.Request().Context()
81+ //
82+ // // This query will be cancelled if context times out
83+ // users, err := db.QueryContext(ctx, "SELECT * FROM users")
84+ // if err != nil {
85+ // if errors.Is(err, context.DeadlineExceeded) {
86+ // return err // Will be converted to 503 by middleware
87+ // }
88+ // return err
89+ // }
90+ //
91+ // return c.JSON(http.StatusOK, users)
92+ // })
93+ //
94+ // ## Long-Running Operation with Context Checking
95+ //
96+ // e.POST("/process", func(c echo.Context) error {
97+ // ctx := c.Request().Context()
98+ //
99+ // // Run operation in goroutine, respecting context
100+ // resultCh := make(chan result)
101+ // errCh := make(chan error)
102+ //
103+ // go func() {
104+ // result, err := processData(ctx) // Context-aware processing
105+ // if err != nil {
106+ // errCh <- err
107+ // return
108+ // }
109+ // resultCh <- result
110+ // }()
111+ //
112+ // select {
113+ // case <-ctx.Done():
114+ // return ctx.Err() // Returns DeadlineExceeded
115+ // case err := <-errCh:
116+ // return err
117+ // case result := <-resultCh:
118+ // return c.JSON(http.StatusOK, result)
119+ // }
120+ // })
121+ //
122+ // ## HTTP Client with Context
123+ //
124+ // e.GET("/proxy", func(c echo.Context) error {
125+ // ctx := c.Request().Context()
126+ //
127+ // req, err := http.NewRequestWithContext(ctx, "GET", "http://api.example.com/data", nil)
128+ // if err != nil {
129+ // return err
130+ // }
131+ //
132+ // client := &http.Client{}
133+ // resp, err := client.Do(req)
134+ // if err != nil {
135+ // if errors.Is(err, context.DeadlineExceeded) {
136+ // return err // Will be converted to 503
137+ // }
138+ // return err
139+ // }
140+ // defer resp.Body.Close()
141+ //
142+ // // Process response...
143+ // return c.String(http.StatusOK, "Proxy response")
144+ // })
145+ //
146+ // # Error Handling
147+ //
148+ // By default, when a context timeout occurs (context.DeadlineExceeded), the middleware:
149+ // - Returns HTTP 503 Service Unavailable
150+ // - Includes the original error as internal error
151+ // - Does NOT write to the response (allows upstream middleware to handle)
152+ //
153+ // # Best Practices
154+ //
155+ // 1. **Use context-aware APIs**: Always use database/HTTP clients that accept context
156+ // 2. **Check context in loops**: For CPU-intensive operations, periodically check ctx.Done()
157+ // 3. **Set appropriate timeouts**: Consider your application's typical response times
158+ // 4. **Handle gracefully**: Provide meaningful error messages to users
159+ // 5. **Place middleware appropriately**: Can be used at any position in middleware chain
160+ //
161+ // # Common Patterns
162+ //
163+ // ## Database Operations
164+ // ctx := c.Request().Context()
165+ // rows, err := db.QueryContext(ctx, query, args...)
166+ //
167+ // ## HTTP Requests
168+ // req, _ := http.NewRequestWithContext(ctx, method, url, body)
169+ // resp, err := client.Do(req)
170+ //
171+ // ## Redis Operations
172+ // result := redisClient.Get(ctx, key)
173+ //
174+ // ## Long-Running Loops
175+ // for {
176+ // select {
177+ // case <-ctx.Done():
178+ // return ctx.Err()
179+ // default:
180+ // // Do work...
181+ // }
182+ // }
15183type ContextTimeoutConfig struct {
16184 // Skipper defines a function to skip middleware.
185+ // Use this to exclude certain endpoints from timeout enforcement.
186+ //
187+ // Example:
188+ // Skipper: func(c echo.Context) bool {
189+ // return c.Request().URL.Path == "/health"
190+ // },
17191 Skipper Skipper
18192
19- // ErrorHandler is a function when error aries in middleware execution.
193+ // ErrorHandler is called when the handler returns an error.
194+ // The default implementation converts context.DeadlineExceeded to HTTP 503.
195+ //
196+ // Use this to customize timeout error responses:
197+ //
198+ // Example:
199+ // ErrorHandler: func(err error, c echo.Context) error {
200+ // if errors.Is(err, context.DeadlineExceeded) {
201+ // return c.JSON(http.StatusRequestTimeout, map[string]string{
202+ // "error": "Operation timed out",
203+ // "timeout": "30s",
204+ // })
205+ // }
206+ // return err
207+ // },
20208 ErrorHandler func (err error , c echo.Context ) error
21209
22- // Timeout configures a timeout for the middleware, defaults to 0 for no timeout
210+ // Timeout configures the request timeout duration.
211+ // REQUIRED - must be greater than 0.
212+ //
213+ // Common values:
214+ // - API endpoints: 30s - 60s
215+ // - File uploads: 5m - 15m
216+ // - Real-time operations: 5s - 10s
217+ // - Background processing: 2m - 5m
218+ //
219+ // Example: 30 * time.Second
23220 Timeout time.Duration
24221}
25222
26- // ContextTimeout returns a middleware which returns error (503 Service Unavailable error) to client
27- // when underlying method returns context.DeadlineExceeded error.
223+ // ContextTimeout returns a middleware that enforces a timeout on request processing.
224+ //
225+ // This is the RECOMMENDED way to handle request timeouts in Echo applications.
226+ // Unlike the deprecated Timeout middleware, this approach:
227+ // - Is safe to use in any middleware position
228+ // - Does not interfere with response writing
229+ // - Relies on cooperative cancellation via context
230+ // - Returns errors instead of writing responses directly
231+ //
232+ // The middleware sets a timeout context on the request and converts any
233+ // context.DeadlineExceeded errors returned by handlers into HTTP 503 responses.
234+ //
235+ // Usage:
236+ //
237+ // e.Use(middleware.ContextTimeout(30 * time.Second))
238+ //
239+ // For handlers to work properly with this middleware, they must:
240+ // - Use context-aware APIs (database, HTTP clients, etc.)
241+ // - Check ctx.Done() in long-running operations
242+ // - Return context.DeadlineExceeded when cancelled
243+ //
244+ // Example handler:
245+ //
246+ // e.GET("/api/data", func(c echo.Context) error {
247+ // ctx := c.Request().Context()
248+ // data, err := db.QueryContext(ctx, "SELECT * FROM data")
249+ // if err != nil {
250+ // return err // DeadlineExceeded will become 503
251+ // }
252+ // return c.JSON(http.StatusOK, data)
253+ // })
254+ //
255+ // See ContextTimeoutConfig documentation for advanced configuration options.
28256func ContextTimeout (timeout time.Duration ) echo.MiddlewareFunc {
29257 return ContextTimeoutWithConfig (ContextTimeoutConfig {Timeout : timeout })
30258}
31259
32- // ContextTimeoutWithConfig returns a Timeout middleware with config.
260+ // ContextTimeoutWithConfig returns a ContextTimeout middleware with custom configuration.
261+ //
262+ // This function allows you to customize timeout behavior including:
263+ // - Custom error handling for timeouts
264+ // - Skipping timeout for specific routes
265+ // - Different timeout durations per route group
266+ //
267+ // See ContextTimeoutConfig documentation for detailed configuration examples.
268+ //
269+ // Example:
270+ //
271+ // e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
272+ // Timeout: 30 * time.Second,
273+ // Skipper: func(c echo.Context) bool {
274+ // return c.Request().URL.Path == "/health"
275+ // },
276+ // ErrorHandler: func(err error, c echo.Context) error {
277+ // if errors.Is(err, context.DeadlineExceeded) {
278+ // return c.JSON(http.StatusRequestTimeout, map[string]string{
279+ // "error": "Request timeout",
280+ // })
281+ // }
282+ // return err
283+ // },
284+ // }))
33285func ContextTimeoutWithConfig (config ContextTimeoutConfig ) echo.MiddlewareFunc {
34286 mw , err := config .ToMiddleware ()
35287 if err != nil {
@@ -38,7 +290,24 @@ func ContextTimeoutWithConfig(config ContextTimeoutConfig) echo.MiddlewareFunc {
38290 return mw
39291}
40292
41- // ToMiddleware converts Config to middleware.
293+ // ToMiddleware converts ContextTimeoutConfig to a middleware function.
294+ //
295+ // This method validates the configuration and returns a ready-to-use middleware.
296+ // It's primarily used internally by ContextTimeoutWithConfig, but can be useful
297+ // for advanced use cases where you need to validate configuration before applying.
298+ //
299+ // Returns an error if:
300+ // - Timeout is 0 or negative
301+ // - Configuration is otherwise invalid
302+ //
303+ // Example:
304+ //
305+ // config := ContextTimeoutConfig{Timeout: 30 * time.Second}
306+ // middleware, err := config.ToMiddleware()
307+ // if err != nil {
308+ // log.Fatal("Invalid timeout config:", err)
309+ // }
310+ // e.Use(middleware)
42311func (config ContextTimeoutConfig ) ToMiddleware () (echo.MiddlewareFunc , error ) {
43312 if config .Timeout == 0 {
44313 return nil , errors .New ("timeout must be set" )
0 commit comments