Skip to content

Commit f57e70b

Browse files
committed
update timeout middleware with warnings
1 parent e8da408 commit f57e70b

File tree

3 files changed

+107
-35
lines changed

3 files changed

+107
-35
lines changed

cookbook/timeouts/server.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ func main() {
1212
// Echo instance
1313
e := echo.New()
1414

15-
// Middleware
16-
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
17-
Timeout: 5 * time.Second,
18-
}))
19-
20-
// Route => handler
21-
e.GET("/", func(c echo.Context) error {
15+
// Handler with timeout middleware
16+
handlerFunc := func(c echo.Context) error {
2217
time.Sleep(10 * time.Second)
2318
return c.String(http.StatusOK, "Hello, World!\n")
19+
}
20+
middlewareFunc := middleware.TimeoutWithConfig(middleware.TimeoutConfig{
21+
Timeout: 30 * time.Second,
22+
ErrorMessage: "my custom error message",
2423
})
24+
e.GET("/", handlerFunc, middlewareFunc)
2525

2626
// Start server
2727
e.Logger.Fatal(e.Start(":1323"))

website/content/guide/http_server.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func main() {
8282

8383
## Auto TLS Server with Let’s Encrypt
8484

85-
See [Auto TLS Recipe](/cooobook/auto-tls#server)
85+
See [Auto TLS Recipe](/cookbook/auto-tls#server)
8686

8787
## HTTP/2 Cleartext Server (HTTP2 over HTTP)
8888

website/content/middleware/timeout.md

+99-27
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,62 @@ description = "Timeout middleware for Echo"
88

99
Timeout middleware is used to timeout at a long running operation within a predefined period.
1010

11+
When timeout occurs, and the client receives timeout response the handler keeps running its code and keeps using resources until it finishes and returns!
12+
13+
> Timeout middleware is a serious performance hit as it buffers all responses from wrapped handler. Do not set it in front of file downloads or responses you want to stream to the client.
14+
15+
Timeout middleware is not a magic wand to hide slow handlers from clients. Consider designing/implementing asynchronous
16+
request/response API if (extremely) fast responses are to be expected and actual work can be done in background
17+
Prefer handling timeouts in handler functions explicitly
18+
1119
*Usage*
1220

13-
`e.Use(middleware.Timeout())`
21+
`e.GET("/", handlerFunc, middleware.Timeout())`
1422

1523
## Custom Configuration
1624

1725
*Usage*
1826

1927
```go
28+
// Echo instance
2029
e := echo.New()
21-
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
22-
Skipper: Skipper,
23-
ErrorHandler: func(err error, e echo.Context) error {
24-
// you can handle your error here, the returning error will be
25-
// passed down the middleware chain
26-
return err
27-
},
28-
Timeout: 30*time.Second,
29-
}))
30+
31+
handlerFunc := func(c echo.Context) error {
32+
time.Sleep(10 * time.Second)
33+
return c.String(http.StatusOK, "Hello, World!\n")
34+
}
35+
middlewareFunc := middleware.TimeoutWithConfig(middleware.TimeoutConfig{
36+
Timeout: 30 * time.Second,
37+
ErrorMessage: "my custom error message",
38+
})
39+
// Handler with timeout middleware
40+
e.GET("/", handlerFunc, middlewareFunc)
3041
```
3142

3243
## Configuration
3344

3445
```go
3546
// TimeoutConfig defines the config for Timeout middleware.
36-
TimeoutConfig struct {
37-
// Skipper defines a function to skip middleware.
38-
Skipper Skipper
39-
// ErrorHandler defines a function which is executed for a timeout
40-
// It can be used to define a custom timeout error
41-
ErrorHandler TimeoutErrorHandlerWithContext
42-
// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
43-
Timeout time.Duration
44-
}
45-
```
47+
type TimeoutConfig struct {
48+
// Skipper defines a function to skip middleware.
49+
Skipper Skipper
4650

47-
*TimeoutErrorHandlerWithContext* is responsible for handling the errors when a timeout happens
48-
```go
49-
// TimeoutErrorHandlerWithContext is an error handler that is used
50-
// with the timeout middleware so we can handle the error
51-
// as we see fit
52-
TimeoutErrorHandlerWithContext func(error, echo.Context) error
51+
// ErrorMessage is written to response on timeout in addition to http.StatusServiceUnavailable (503) status code
52+
// It can be used to define a custom timeout error message
53+
ErrorMessage string
54+
55+
// OnTimeoutRouteErrorHandler is an error handler that is executed for error that was returned from wrapped route after
56+
// request timeouted and we already had sent the error code (503) and message response to the client.
57+
// NB: do not write headers/body inside this handler. The response has already been sent to the client and response writer
58+
// will not accept anything no more. If you want to know what actual route middleware timeouted use `c.Path()`
59+
OnTimeoutRouteErrorHandler func(err error, c echo.Context)
60+
61+
// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
62+
// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
63+
// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
64+
// difference over 500microseconds (0.5millisecond) response seems to be reliable
65+
Timeout time.Duration
66+
}
5367
```
5468

5569
*Default Configuration*
@@ -58,6 +72,64 @@ TimeoutErrorHandlerWithContext func(error, echo.Context) error
5872
DefaultTimeoutConfig = TimeoutConfig{
5973
Skipper: DefaultSkipper,
6074
Timeout: 0,
61-
ErrorHandler: nil,
75+
ErrorMessage: "",
76+
}
77+
```
78+
79+
## Alternatively handle timeouts in handlers
80+
81+
```go
82+
func main() {
83+
e := echo.New()
84+
85+
doBusinessLogic := func(ctx context.Context, UID string) error {
86+
// NB: Do not use echo.JSON() or any other method that writes data/headers to client here. This function is executed
87+
// in different coroutine that should not access echo.Context and response writer
88+
89+
log.Printf("uid: %v\n", UID)
90+
//res, err := slowDatabaseCon.ExecContext(ctx, query, args)
91+
time.Sleep(10 * time.Second) // simulate slow execution
92+
log.Print("doBusinessLogic done\n")
93+
return nil
94+
}
95+
96+
handlerFunc := func(c echo.Context) error {
97+
defer log.Print("handlerFunc done\n")
98+
99+
// extract and validate needed data from request and pass it to business function
100+
UID := c.QueryParam("uid")
101+
102+
ctx, cancel := context.WithTimeout(c.Request().Context(), 5 * time.Second)
103+
defer cancel()
104+
result := make(chan error)
105+
go func() { // run actual business logic in separate coroutine
106+
defer func() { // unhandled panic in coroutine will crash the whole application
107+
if err := recover(); err != nil {
108+
result <- fmt.Errorf("panic: %v", err)
109+
}
110+
}()
111+
result <- doBusinessLogic(ctx, UID)
112+
}()
113+
114+
select { // wait until doBusinessLogic finishes or we timeout while waiting for the result
115+
case <-ctx.Done():
116+
err := ctx.Err()
117+
if err == context.DeadlineExceeded {
118+
return echo.NewHTTPError(http.StatusServiceUnavailable, "doBusinessLogic timeout")
119+
}
120+
return err // probably client closed the connection
121+
case err := <-result: // doBusinessLogic finishes
122+
if err != nil {
123+
return err
124+
}
125+
}
126+
return c.NoContent(http.StatusAccepted)
127+
}
128+
e.GET("/", handlerFunc)
129+
130+
s := http.Server{Addr: ":8080", Handler: e}
131+
if err := s.ListenAndServe(); err != http.ErrServerClosed {
132+
log.Fatal(err)
133+
}
62134
}
63135
```

0 commit comments

Comments
 (0)