From a1726856008d9396866e3cf40114b7a154b1cc3e Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:38:44 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20add=20timeoutcont?= =?UTF-8?q?ext=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/timeoutcontext/README.md | 83 ++++++++++++++++ middleware/timeoutcontext/timeoutcontext.go | 31 ++++++ .../timeoutcontext/timeoutcontext_test.go | 97 +++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 middleware/timeoutcontext/README.md create mode 100644 middleware/timeoutcontext/timeoutcontext.go create mode 100644 middleware/timeoutcontext/timeoutcontext_test.go diff --git a/middleware/timeoutcontext/README.md b/middleware/timeoutcontext/README.md new file mode 100644 index 0000000000..affbc3230e --- /dev/null +++ b/middleware/timeoutcontext/README.md @@ -0,0 +1,83 @@ +# Timeout Context +Timeout Context middleware for Fiber. As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. + +If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. + +It has no race conditions, ready to use on production. + +### Table of Contents +- [Timeout Context](#timeout-context) + - [Table of Contents](#table-of-contents) + - [Signatures](#signatures) + - [Examples](#examples) + + +### Signatures +```go +func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/timeoutcontext" +) +``` + +After you initiate your Fiber app, you can use: +```go +h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + +app.Get("/foo", timeoutcontext.New(h, 5 * time.Second)) + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return context.DeadlineExceeded + case <-timer.C: + } + return nil +} + +``` + +Use with custom error: +```go +var ErrFooTimeOut = errors.New("foo context canceled") + +h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + +app.Get("/foo", timeoutcontext.New(h, 5 * time.Second), ErrFooTimeOut) + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ErrFooTimeOut + case <-timer.C: + } + return nil +} + +``` diff --git a/middleware/timeoutcontext/timeoutcontext.go b/middleware/timeoutcontext/timeoutcontext.go new file mode 100644 index 0000000000..3f30d88eac --- /dev/null +++ b/middleware/timeoutcontext/timeoutcontext.go @@ -0,0 +1,31 @@ +package timeoutcontext + +import ( + "context" + "errors" + "time" + + "github.com/gofiber/fiber/v2" +) + +// New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. +func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler { + return func(ctx *fiber.Ctx) error { + timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), timeout) + defer cancel() + ctx.SetUserContext(timeoutContext) + if err := handler(ctx); err != nil { + unwrappedErr := errors.Unwrap(err) + if unwrappedErr == context.DeadlineExceeded { + return fiber.ErrRequestTimeout + } + for i := range timeoutErrors { + if unwrappedErr == timeoutErrors[i] { + return fiber.ErrRequestTimeout + } + } + return err + } + return nil + } +} diff --git a/middleware/timeoutcontext/timeoutcontext_test.go b/middleware/timeoutcontext/timeoutcontext_test.go new file mode 100644 index 0000000000..7efa3b9764 --- /dev/null +++ b/middleware/timeoutcontext/timeoutcontext_test.go @@ -0,0 +1,97 @@ +package timeoutcontext + +import ( + "context" + "errors" + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_TimeoutContext +func Test_TimeoutContext(t *testing.T) { + // fiber instance + app := fiber.New() + h := New(func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + }, 100*time.Millisecond) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} + +var ErrFooTimeOut = errors.New("foo context canceled") + +// go test -run Test_TimeoutContextWithCustomError +func Test_TimeoutContextWithCustomError(t *testing.T) { + // fiber instance + app := fiber.New() + h := New(func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + }, 100*time.Millisecond, ErrFooTimeOut) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return context.DeadlineExceeded + case <-timer.C: + } + return nil +} + +func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ErrFooTimeOut + case <-timer.C: + } + return nil +} From 766c7fe38573b478c700442b6e0cf195fab7b09c Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:42:55 +0300 Subject: [PATCH 2/9] move timeoutconext to timeout package --- middleware/timeout/README.md | 80 ++++++++++++++++++- .../timeoutcontext.go | 15 ++-- .../timeoutcontext_test.go | 29 ++----- 3 files changed, 93 insertions(+), 31 deletions(-) rename middleware/{timeoutcontext => timeout}/timeoutcontext.go (59%) rename middleware/{timeoutcontext => timeout}/timeoutcontext_test.go (76%) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index 4406d1ffac..b61987da52 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -1,14 +1,25 @@ # Timeout Timeout middleware for [Fiber](https://github.com/gofiber/fiber) wraps a `fiber.Handler` with a timeout. If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). +Also has timeout with context middleware by `NewWithContext`, it creates a context with `context.WithTimeout` and pass it in `UserContext`. + +If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, it cancels current operations using context and the timeout error is set and forwarded to the centralized `ErrorHandler`. `UserContext` needs to be passed all long running operations. + +`NewWithContext` has no race conditions, ready to use on production. + + ### Table of Contents -- [Signatures](#signatures) -- [Examples](#examples) +- [Timeout](#timeout) + - [Table of Contents](#table-of-contents) + - [Signatures](#signatures) + - [Examples](#examples) + - [NewWithContext Examples](#newwithcontext-examples) ### Signatures ```go func New(h fiber.Handler, t time.Duration) fiber.Handler +func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler ``` ### Examples @@ -32,3 +43,68 @@ handler := func(ctx *fiber.Ctx) error { app.Get("/foo", timeout.New(handler, 5 * time.Second)) ``` + +### NewWithContext Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/timeout" +) +``` + +After you initiate your Fiber app, you can use: +```go +h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + +app.Get("/foo", timeoutcontext.NewWithContext(h, 5 * time.Second)) + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return context.DeadlineExceeded + case <-timer.C: + } + return nil +} + +``` + +Use with custom error: +```go +var ErrFooTimeOut = errors.New("foo context canceled") + +h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + +app.Get("/foo", timeoutcontext.NewWithContext(h, 5 * time.Second), ErrFooTimeOut) + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ErrFooTimeOut + case <-timer.C: + } + return nil +} + +``` diff --git a/middleware/timeoutcontext/timeoutcontext.go b/middleware/timeout/timeoutcontext.go similarity index 59% rename from middleware/timeoutcontext/timeoutcontext.go rename to middleware/timeout/timeoutcontext.go index 3f30d88eac..eed2d72e4d 100644 --- a/middleware/timeoutcontext/timeoutcontext.go +++ b/middleware/timeout/timeoutcontext.go @@ -1,4 +1,4 @@ -package timeoutcontext +package timeout import ( "context" @@ -9,18 +9,17 @@ import ( ) // New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. -func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler { +func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { return func(ctx *fiber.Ctx) error { - timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), timeout) + timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) defer cancel() ctx.SetUserContext(timeoutContext) - if err := handler(ctx); err != nil { - unwrappedErr := errors.Unwrap(err) - if unwrappedErr == context.DeadlineExceeded { + if err := h(ctx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { return fiber.ErrRequestTimeout } - for i := range timeoutErrors { - if unwrappedErr == timeoutErrors[i] { + for i := range tErrs { + if errors.Is(err, tErrs[i]) { return fiber.ErrRequestTimeout } } diff --git a/middleware/timeoutcontext/timeoutcontext_test.go b/middleware/timeout/timeoutcontext_test.go similarity index 76% rename from middleware/timeoutcontext/timeoutcontext_test.go rename to middleware/timeout/timeoutcontext_test.go index 7efa3b9764..ac53fa3c5b 100644 --- a/middleware/timeoutcontext/timeoutcontext_test.go +++ b/middleware/timeout/timeoutcontext_test.go @@ -1,4 +1,4 @@ -package timeoutcontext +package timeout import ( "context" @@ -16,10 +16,10 @@ import ( func Test_TimeoutContext(t *testing.T) { // fiber instance app := fiber.New() - h := New(func(c *fiber.Ctx) error { + h := NewWithContext(func(c *fiber.Ctx) error { sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) + if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { + return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) } return nil }, 100*time.Millisecond) @@ -46,9 +46,9 @@ var ErrFooTimeOut = errors.New("foo context canceled") func Test_TimeoutContextWithCustomError(t *testing.T) { // fiber instance app := fiber.New() - h := New(func(c *fiber.Ctx) error { + h := NewWithContext(func(c *fiber.Ctx) error { sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { return fmt.Errorf("%w: execution error", err) } return nil @@ -70,27 +70,14 @@ func Test_TimeoutContextWithCustomError(t *testing.T) { testSucces("30") } -func sleepWithContext(ctx context.Context, d time.Duration) error { - timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return context.DeadlineExceeded - case <-timer.C: - } - return nil -} - -func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error { +func sleepWithContext(ctx context.Context, d time.Duration, te error) error { timer := time.NewTimer(d) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } - return ErrFooTimeOut + return te case <-timer.C: } return nil From 351c44583d998a80909d974575632cd35de51941 Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Thu, 15 Sep 2022 16:07:22 +0300 Subject: [PATCH 3/9] remove timeoutcontext readme.md --- middleware/timeoutcontext/README.md | 83 ----------------------------- 1 file changed, 83 deletions(-) delete mode 100644 middleware/timeoutcontext/README.md diff --git a/middleware/timeoutcontext/README.md b/middleware/timeoutcontext/README.md deleted file mode 100644 index affbc3230e..0000000000 --- a/middleware/timeoutcontext/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Timeout Context -Timeout Context middleware for Fiber. As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. - -If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. - -It has no race conditions, ready to use on production. - -### Table of Contents -- [Timeout Context](#timeout-context) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - -### Signatures -```go -func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler -``` - -### Examples -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/timeoutcontext" -) -``` - -After you initiate your Fiber app, you can use: -```go -h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil - } - -app.Get("/foo", timeoutcontext.New(h, 5 * time.Second)) - -func sleepWithContext(ctx context.Context, d time.Duration) error { - timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return context.DeadlineExceeded - case <-timer.C: - } - return nil -} - -``` - -Use with custom error: -```go -var ErrFooTimeOut = errors.New("foo context canceled") - -h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil - } - -app.Get("/foo", timeoutcontext.New(h, 5 * time.Second), ErrFooTimeOut) - -func sleepWithContext(ctx context.Context, d time.Duration) error { - timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return ErrFooTimeOut - case <-timer.C: - } - return nil -} - -``` From 550ce686ba075ea758e443056fc05763ef364da3 Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:18:38 +0300 Subject: [PATCH 4/9] replace timeout mware with timeout context mware --- middleware/timeout/README.md | 43 ++------ middleware/timeout/timeout.go | 47 +++----- middleware/timeout/timeout_test.go | 125 +++++++++++++--------- middleware/timeout/timeoutcontext.go | 30 ------ middleware/timeout/timeoutcontext_test.go | 84 --------------- 5 files changed, 101 insertions(+), 228 deletions(-) delete mode 100644 middleware/timeout/timeoutcontext.go delete mode 100644 middleware/timeout/timeoutcontext_test.go diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index b61987da52..fcdef1bed0 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -1,25 +1,20 @@ # Timeout -Timeout middleware for [Fiber](https://github.com/gofiber/fiber) wraps a `fiber.Handler` with a timeout. If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). +Timeout middleware for Fiber. As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. -Also has timeout with context middleware by `NewWithContext`, it creates a context with `context.WithTimeout` and pass it in `UserContext`. - -If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, it cancels current operations using context and the timeout error is set and forwarded to the centralized `ErrorHandler`. `UserContext` needs to be passed all long running operations. - -`NewWithContext` has no race conditions, ready to use on production. +If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. +It has no race conditions, ready to use on production. ### Table of Contents - [Timeout](#timeout) - [Table of Contents](#table-of-contents) - [Signatures](#signatures) - [Examples](#examples) - - [NewWithContext Examples](#newwithcontext-examples) ### Signatures ```go -func New(h fiber.Handler, t time.Duration) fiber.Handler -func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler +func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler ``` ### Examples @@ -31,28 +26,6 @@ import ( ) ``` -After you initiate your Fiber app, you can use the following possibilities: -```go -handler := func(ctx *fiber.Ctx) error { - err := ctx.SendString("Hello, World 👋!") - if err != nil { - return err - } - return nil -} - -app.Get("/foo", timeout.New(handler, 5 * time.Second)) -``` - -### NewWithContext Examples -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/timeout" -) -``` - After you initiate your Fiber app, you can use: ```go h := func(c *fiber.Ctx) error { @@ -63,7 +36,7 @@ h := func(c *fiber.Ctx) error { return nil } -app.Get("/foo", timeoutcontext.NewWithContext(h, 5 * time.Second)) +app.Get("/foo", timeoutcontext.New(h, 5 * time.Second)) func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) @@ -77,7 +50,6 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { } return nil } - ``` Use with custom error: @@ -92,7 +64,7 @@ h := func(c *fiber.Ctx) error { return nil } -app.Get("/foo", timeoutcontext.NewWithContext(h, 5 * time.Second), ErrFooTimeOut) +app.Get("/foo", timeoutcontext.New(h, 5 * time.Second), ErrFooTimeOut) func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) @@ -106,5 +78,4 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { } return nil } - -``` +``` \ No newline at end of file diff --git a/middleware/timeout/timeout.go b/middleware/timeout/timeout.go index 6cb980aa01..9f1fd21997 100644 --- a/middleware/timeout/timeout.go +++ b/middleware/timeout/timeout.go @@ -1,43 +1,30 @@ package timeout import ( - "fmt" - "sync" + "context" + "errors" "time" "github.com/gofiber/fiber/v2" ) -var once sync.Once - -// New wraps a handler and aborts the process of the handler if the timeout is reached -func New(handler fiber.Handler, timeout time.Duration) fiber.Handler { - once.Do(func() { - fmt.Println("[Warning] timeout contains data race issues, not ready for production!") - }) - - if timeout <= 0 { - return handler - } - - // logic is from fasthttp.TimeoutWithCodeHandler https://github.com/valyala/fasthttp/blob/master/server.go#L418 +// New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. +func New(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { return func(ctx *fiber.Ctx) error { - ch := make(chan struct{}, 1) - - go func() { - defer func() { - _ = recover() - }() - _ = handler(ctx) - ch <- struct{}{} - }() - - select { - case <-ch: - case <-time.After(timeout): - return fiber.ErrRequestTimeout + timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) + defer cancel() + ctx.SetUserContext(timeoutContext) + if err := h(ctx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return fiber.ErrRequestTimeout + } + for i := range tErrs { + if errors.Is(err, tErrs[i]) { + return fiber.ErrRequestTimeout + } + } + return err } - return nil } } diff --git a/middleware/timeout/timeout_test.go b/middleware/timeout/timeout_test.go index dc60e2eba5..225eabc152 100644 --- a/middleware/timeout/timeout_test.go +++ b/middleware/timeout/timeout_test.go @@ -1,55 +1,84 @@ package timeout -// // go test -run Test_Middleware_Timeout -// func Test_Middleware_Timeout(t *testing.T) { -// app := fiber.New(fiber.Config{DisableStartupMessage: true}) +import ( + "context" + "errors" + "fmt" + "net/http/httptest" + "testing" + "time" -// h := New(func(c *fiber.Ctx) error { -// sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") -// time.Sleep(sleepTime) -// return c.SendString("After " + c.Params("sleepTime") + "ms sleeping") -// }, 5*time.Millisecond) -// app.Get("/test/:sleepTime", h) + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) -// testTimeout := func(timeoutStr string) { -// resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) -// utils.AssertEqual(t, nil, err, "app.Test(req)") -// utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") +// go test -run Test_Timeout +func Test_Timeout(t *testing.T) { + // fiber instance + app := fiber.New() + h := New(func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { + return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) + } + return nil + }, 100*time.Millisecond) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} -// body, err := ioutil.ReadAll(resp.Body) -// utils.AssertEqual(t, nil, err) -// utils.AssertEqual(t, "Request Timeout", string(body)) -// } -// testSucces := func(timeoutStr string) { -// resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) -// utils.AssertEqual(t, nil, err, "app.Test(req)") -// utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") +var ErrFooTimeOut = errors.New("foo context canceled") -// body, err := ioutil.ReadAll(resp.Body) -// utils.AssertEqual(t, nil, err) -// utils.AssertEqual(t, "After "+timeoutStr+"ms sleeping", string(body)) -// } +// go test -run Test_TimeoutWithCustomError +func Test_TimeoutWithCustomError(t *testing.T) { + // fiber instance + app := fiber.New() + h := New(func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + }, 100*time.Millisecond, ErrFooTimeOut) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} -// testTimeout("15") -// testSucces("2") -// testTimeout("30") -// testSucces("3") -// } - -// // go test -run -v Test_Timeout_Panic -// func Test_Timeout_Panic(t *testing.T) { -// app := fiber.New(fiber.Config{DisableStartupMessage: true}) - -// app.Get("/panic", recover.New(), New(func(c *fiber.Ctx) error { -// c.Set("dummy", "this should not be here") -// panic("panic in timeout handler") -// }, 5*time.Millisecond)) - -// resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) -// utils.AssertEqual(t, nil, err, "app.Test(req)") -// utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") - -// body, err := ioutil.ReadAll(resp.Body) -// utils.AssertEqual(t, nil, err) -// utils.AssertEqual(t, "Request Timeout", string(body)) -// } +func sleepWithContext(ctx context.Context, d time.Duration, te error) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return te + case <-timer.C: + } + return nil +} diff --git a/middleware/timeout/timeoutcontext.go b/middleware/timeout/timeoutcontext.go deleted file mode 100644 index eed2d72e4d..0000000000 --- a/middleware/timeout/timeoutcontext.go +++ /dev/null @@ -1,30 +0,0 @@ -package timeout - -import ( - "context" - "errors" - "time" - - "github.com/gofiber/fiber/v2" -) - -// New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. -func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { - return func(ctx *fiber.Ctx) error { - timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) - defer cancel() - ctx.SetUserContext(timeoutContext) - if err := h(ctx); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return fiber.ErrRequestTimeout - } - for i := range tErrs { - if errors.Is(err, tErrs[i]) { - return fiber.ErrRequestTimeout - } - } - return err - } - return nil - } -} diff --git a/middleware/timeout/timeoutcontext_test.go b/middleware/timeout/timeoutcontext_test.go deleted file mode 100644 index ac53fa3c5b..0000000000 --- a/middleware/timeout/timeoutcontext_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package timeout - -import ( - "context" - "errors" - "fmt" - "net/http/httptest" - "testing" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/utils" -) - -// go test -run Test_TimeoutContext -func Test_TimeoutContext(t *testing.T) { - // fiber instance - app := fiber.New() - h := NewWithContext(func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { - return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) - } - return nil - }, 100*time.Millisecond) - app.Get("/test/:sleepTime", h) - testTimeout := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") - } - testSucces := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - } - testTimeout("300") - testTimeout("500") - testSucces("50") - testSucces("30") -} - -var ErrFooTimeOut = errors.New("foo context canceled") - -// go test -run Test_TimeoutContextWithCustomError -func Test_TimeoutContextWithCustomError(t *testing.T) { - // fiber instance - app := fiber.New() - h := NewWithContext(func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil - }, 100*time.Millisecond, ErrFooTimeOut) - app.Get("/test/:sleepTime", h) - testTimeout := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") - } - testSucces := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - } - testTimeout("300") - testTimeout("500") - testSucces("50") - testSucces("30") -} - -func sleepWithContext(ctx context.Context, d time.Duration, te error) error { - timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return te - case <-timer.C: - } - return nil -} From 9e3822cbf3d09eb59533f16904b3dac7ba2957af Mon Sep 17 00:00:00 2001 From: RW Date: Fri, 16 Sep 2022 13:50:33 +0200 Subject: [PATCH 5/9] Update README.md --- middleware/timeout/README.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index fcdef1bed0..5adab8dbfd 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -6,10 +6,8 @@ If the context passed executions (eg. DB ops, Http calls) takes longer than the It has no race conditions, ready to use on production. ### Table of Contents -- [Timeout](#timeout) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) +- [Signatures](#signatures) +- [Examples](#examples) ### Signatures @@ -29,12 +27,12 @@ import ( After you initiate your Fiber app, you can use: ```go h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) } + return nil +} app.Get("/foo", timeoutcontext.New(h, 5 * time.Second)) @@ -57,12 +55,12 @@ Use with custom error: var ErrFooTimeOut = errors.New("foo context canceled") h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) } + return nil +} app.Get("/foo", timeoutcontext.New(h, 5 * time.Second), ErrFooTimeOut) @@ -78,4 +76,4 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { } return nil } -``` \ No newline at end of file +``` From 23c92752fb3112cb937f56da29efc19c7db4c9fc Mon Sep 17 00:00:00 2001 From: RW Date: Fri, 16 Sep 2022 13:51:47 +0200 Subject: [PATCH 6/9] Update README.md --- middleware/timeout/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index 5adab8dbfd..82b1844208 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -34,7 +34,7 @@ h := func(c *fiber.Ctx) error { return nil } -app.Get("/foo", timeoutcontext.New(h, 5 * time.Second)) +app.Get("/foo", timeout.New(h, 5 * time.Second)) func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) @@ -62,7 +62,7 @@ h := func(c *fiber.Ctx) error { return nil } -app.Get("/foo", timeoutcontext.New(h, 5 * time.Second), ErrFooTimeOut) +app.Get("/foo", timeout.New(h, 5 * time.Second), ErrFooTimeOut) func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) From 3f34fc33e7e61171e17d060b3df68b7fcdbd48e5 Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Fri, 16 Sep 2022 16:07:27 +0300 Subject: [PATCH 7/9] update timeout middleware readme --- middleware/timeout/README.md | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index 82b1844208..f1cb4ee650 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -26,18 +26,23 @@ import ( After you initiate your Fiber app, you can use: ```go -h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) +func main() { + app := fiber.New() + h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil } - return nil -} -app.Get("/foo", timeout.New(h, 5 * time.Second)) + app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second)) + _ = app.Listen(":3000") +} func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) + select { case <-ctx.Done(): if !timer.Stop() { @@ -50,19 +55,29 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { } ``` -Use with custom error: +Test it with curl: +```bash +curl --location --request GET 'http://localhost:3000/foo?sleepTime=1000' // http 200 (OK) +curl --location --request GET 'http://localhost:3000/foo?sleepTime=3000' // http 408 (Request Timeout) +``` + +When using with custom error: ```go var ErrFooTimeOut = errors.New("foo context canceled") -h := func(c *fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) +func main() { + app := fiber.New() + h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil } - return nil -} -app.Get("/foo", timeout.New(h, 5 * time.Second), ErrFooTimeOut) + app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second), ErrFooTimeOut) + _ = app.Listen(":3000") +} func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) From eb5cbeb032d31187972a08611c3c616b60fcc656 Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Fri, 16 Sep 2022 16:13:29 +0300 Subject: [PATCH 8/9] test curl commands fixed --- middleware/timeout/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index f1cb4ee650..1118b8c467 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -55,12 +55,17 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { } ``` -Test it with curl: +Test http 200 with curl: ```bash -curl --location --request GET 'http://localhost:3000/foo?sleepTime=1000' // http 200 (OK) -curl --location --request GET 'http://localhost:3000/foo?sleepTime=3000' // http 408 (Request Timeout) +curl --location -I --request GET 'http://localhost:3000/foo/1000' ``` +Test http 408 with curl: +```bash +curl --location -I --request GET 'http://localhost:3000/foo/3000' +``` + + When using with custom error: ```go var ErrFooTimeOut = errors.New("foo context canceled") From b90f286251702b072109581fa07ec4f257d1cf36 Mon Sep 17 00:00:00 2001 From: Hakan Kutluay <77051856+hakankutluay@users.noreply.github.com> Date: Fri, 16 Sep 2022 16:25:39 +0300 Subject: [PATCH 9/9] rename sample code title on timeout middleware --- middleware/timeout/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md index 1118b8c467..4cc68d0710 100644 --- a/middleware/timeout/README.md +++ b/middleware/timeout/README.md @@ -24,7 +24,7 @@ import ( ) ``` -After you initiate your Fiber app, you can use: +Sample timeout middleware usage ```go func main() { app := fiber.New()