Skip to content

Commit

Permalink
update timeout middleware with warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Jul 3, 2021
1 parent e8da408 commit f57e70b
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 35 deletions.
14 changes: 7 additions & 7 deletions cookbook/timeouts/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ func main() {
// Echo instance
e := echo.New()

// Middleware
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 5 * time.Second,
}))

// Route => handler
e.GET("/", func(c echo.Context) error {
// Handler with timeout middleware
handlerFunc := func(c echo.Context) error {
time.Sleep(10 * time.Second)
return c.String(http.StatusOK, "Hello, World!\n")
}
middlewareFunc := middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
ErrorMessage: "my custom error message",
})
e.GET("/", handlerFunc, middlewareFunc)

// Start server
e.Logger.Fatal(e.Start(":1323"))
Expand Down
2 changes: 1 addition & 1 deletion website/content/guide/http_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func main() {

## Auto TLS Server with Let’s Encrypt

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

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

Expand Down
126 changes: 99 additions & 27 deletions website/content/middleware/timeout.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,62 @@ description = "Timeout middleware for Echo"

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

When timeout occurs, and the client receives timeout response the handler keeps running its code and keeps using resources until it finishes and returns!

> 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.
Timeout middleware is not a magic wand to hide slow handlers from clients. Consider designing/implementing asynchronous
request/response API if (extremely) fast responses are to be expected and actual work can be done in background
Prefer handling timeouts in handler functions explicitly

*Usage*

`e.Use(middleware.Timeout())`
`e.GET("/", handlerFunc, middleware.Timeout())`

## Custom Configuration

*Usage*

```go
// Echo instance
e := echo.New()
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Skipper: Skipper,
ErrorHandler: func(err error, e echo.Context) error {
// you can handle your error here, the returning error will be
// passed down the middleware chain
return err
},
Timeout: 30*time.Second,
}))

handlerFunc := func(c echo.Context) error {
time.Sleep(10 * time.Second)
return c.String(http.StatusOK, "Hello, World!\n")
}
middlewareFunc := middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
ErrorMessage: "my custom error message",
})
// Handler with timeout middleware
e.GET("/", handlerFunc, middlewareFunc)
```

## Configuration

```go
// TimeoutConfig defines the config for Timeout middleware.
TimeoutConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// ErrorHandler defines a function which is executed for a timeout
// It can be used to define a custom timeout error
ErrorHandler TimeoutErrorHandlerWithContext
// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
Timeout time.Duration
}
```
type TimeoutConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper

*TimeoutErrorHandlerWithContext* is responsible for handling the errors when a timeout happens
```go
// TimeoutErrorHandlerWithContext is an error handler that is used
// with the timeout middleware so we can handle the error
// as we see fit
TimeoutErrorHandlerWithContext func(error, echo.Context) error
// ErrorMessage is written to response on timeout in addition to http.StatusServiceUnavailable (503) status code
// It can be used to define a custom timeout error message
ErrorMessage string

// OnTimeoutRouteErrorHandler is an error handler that is executed for error that was returned from wrapped route after
// request timeouted and we already had sent the error code (503) and message response to the client.
// NB: do not write headers/body inside this handler. The response has already been sent to the client and response writer
// will not accept anything no more. If you want to know what actual route middleware timeouted use `c.Path()`
OnTimeoutRouteErrorHandler func(err error, c echo.Context)

// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
// difference over 500microseconds (0.5millisecond) response seems to be reliable
Timeout time.Duration
}
```

*Default Configuration*
Expand All @@ -58,6 +72,64 @@ TimeoutErrorHandlerWithContext func(error, echo.Context) error
DefaultTimeoutConfig = TimeoutConfig{
Skipper: DefaultSkipper,
Timeout: 0,
ErrorHandler: nil,
ErrorMessage: "",
}
```

## Alternatively handle timeouts in handlers

```go
func main() {
e := echo.New()

doBusinessLogic := func(ctx context.Context, UID string) error {
// NB: Do not use echo.JSON() or any other method that writes data/headers to client here. This function is executed
// in different coroutine that should not access echo.Context and response writer

log.Printf("uid: %v\n", UID)
//res, err := slowDatabaseCon.ExecContext(ctx, query, args)
time.Sleep(10 * time.Second) // simulate slow execution
log.Print("doBusinessLogic done\n")
return nil
}

handlerFunc := func(c echo.Context) error {
defer log.Print("handlerFunc done\n")

// extract and validate needed data from request and pass it to business function
UID := c.QueryParam("uid")

ctx, cancel := context.WithTimeout(c.Request().Context(), 5 * time.Second)
defer cancel()
result := make(chan error)
go func() { // run actual business logic in separate coroutine
defer func() { // unhandled panic in coroutine will crash the whole application
if err := recover(); err != nil {
result <- fmt.Errorf("panic: %v", err)
}
}()
result <- doBusinessLogic(ctx, UID)
}()

select { // wait until doBusinessLogic finishes or we timeout while waiting for the result
case <-ctx.Done():
err := ctx.Err()
if err == context.DeadlineExceeded {
return echo.NewHTTPError(http.StatusServiceUnavailable, "doBusinessLogic timeout")
}
return err // probably client closed the connection
case err := <-result: // doBusinessLogic finishes
if err != nil {
return err
}
}
return c.NoContent(http.StatusAccepted)
}
e.GET("/", handlerFunc)

s := http.Server{Addr: ":8080", Handler: e}
if err := s.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
```

0 comments on commit f57e70b

Please sign in to comment.