Skip to content

Commit 073b8de

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

File tree

3 files changed

+100
-35
lines changed

3 files changed

+100
-35
lines changed

Diff for: 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"))

Diff for: 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

Diff for: website/content/middleware/timeout.md

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

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

11+
> Note: 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 not a magic wand to hide slow handlers from clients. Consider designing/implementing asynchronous
14+
> request/response API if (extremely) fast responses are to be expected and actual work can be done in background
15+
> Prefer handling timeouts in handler functions explicitly
16+
1117
*Usage*
1218

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

1521
## Custom Configuration
1622

1723
*Usage*
1824

1925
```go
26+
// Echo instance
2027
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-
}))
28+
29+
handlerFunc := func(c echo.Context) error {
30+
time.Sleep(10 * time.Second)
31+
return c.String(http.StatusOK, "Hello, World!\n")
32+
}
33+
middlewareFunc := middleware.TimeoutWithConfig(middleware.TimeoutConfig{
34+
Timeout: 30 * time.Second,
35+
ErrorMessage: "my custom error message",
36+
})
37+
// Handler with timeout middleware
38+
e.GET("/", handlerFunc, middlewareFunc)
3039
```
3140

3241
## Configuration
3342

3443
```go
3544
// 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-
```
45+
type TimeoutConfig struct {
46+
// Skipper defines a function to skip middleware.
47+
Skipper Skipper
4648

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

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

0 commit comments

Comments
 (0)