diff --git a/cookbook/timeouts/server.go b/cookbook/timeouts/server.go index fd7c9476..92b96a79 100644 --- a/cookbook/timeouts/server.go +++ b/cookbook/timeouts/server.go @@ -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")) diff --git a/website/content/guide/http_server.md b/website/content/guide/http_server.md index 6be3193f..42b711b1 100644 --- a/website/content/guide/http_server.md +++ b/website/content/guide/http_server.md @@ -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) diff --git a/website/content/middleware/timeout.md b/website/content/middleware/timeout.md index 1c4a31ec..fbc102b3 100644 --- a/website/content/middleware/timeout.md +++ b/website/content/middleware/timeout.md @@ -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* @@ -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) + } } ```