Skip to content

Commit 9c11b31

Browse files
vishrclaude
andcommitted
Document ContextTimeout middleware with comprehensive examples
Addresses issue #2745 by providing complete documentation for the ContextTimeout middleware, which was previously undocumented despite being the recommended approach over the deprecated Timeout middleware. **Documentation Added:** **Overview & Key Differences:** - Clear explanation of why ContextTimeout is preferred over Timeout middleware - Highlights safety improvements (no response writer interference, no data races) - Explains cooperative cancellation model **Configuration Examples:** - Basic usage with simple timeout - Custom error handling for timeout responses - Route-specific skipping with Skipper - Advanced configuration patterns **Handler Examples (3 detailed scenarios):** - Context-aware database queries with proper error handling - Long-running operations using goroutines and select statements - HTTP client requests with context propagation **Best Practices & Common Patterns:** - Database operations: `db.QueryContext(ctx, ...)` - HTTP requests: `http.NewRequestWithContext(ctx, ...)` - Redis operations: `client.Get(ctx, key)` - CPU-intensive loops with context checking **Enhanced Field Documentation:** - Detailed explanations for Skipper, ErrorHandler, and Timeout fields - Examples for each configuration option - Recommended timeout values for different use cases **Function Documentation:** - Comprehensive ContextTimeout() documentation with usage examples - Enhanced ContextTimeoutWithConfig() with advanced patterns - ToMiddleware() method documentation for validation scenarios This resolves user confusion about which timeout middleware to use and provides practical examples showing how handlers should be implemented to work effectively with context-based timeouts. Fixes #2745 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 5a3f2ac commit 9c11b31

File tree

1 file changed

+275
-6
lines changed

1 file changed

+275
-6
lines changed

middleware/context_timeout.go

Lines changed: 275 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
// }
15183
type 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.
28256
func 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+
// }))
33285
func 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)
42311
func (config ContextTimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
43312
if config.Timeout == 0 {
44313
return nil, errors.New("timeout must be set")

0 commit comments

Comments
 (0)