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 2, 2021
1 parent e8da408 commit 073b8de
Show file tree
Hide file tree
Showing 3 changed files with 100 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
119 changes: 92 additions & 27 deletions website/content/middleware/timeout.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,60 @@ description = "Timeout middleware for Echo"

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

> 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!
> 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 +70,59 @@ 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
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 073b8de

Please sign in to comment.