Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend default timeout on certain path patterns #53

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.idea
.idea
.vscode

# Environment
.env
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,49 @@ func main() {
}
}
```

### extended timeout on certain paths

```go
package main

import (
"log"
"net/http"
"time"

"github.com/gin-contrib/timeout"
"github.com/gin-gonic/gin"
)

func testResponse(c *gin.Context) {
c.String(http.StatusRequestTimeout, "timeout")
}

func extendedTimeoutMiddleware() gin.HandlerFunc {
return timeout.New(
timeout.WithTimeout(200*time.Millisecond), // Default timeout on all routes
timeout.WithExtendedTimeout(1000*time.Millisecond), // Extended timeout on pattern based routes
timeout.WithExtendedPaths([]string{
"/ext.*", // List of patterns to allow extended timeouts
}),
timeout.WithHandler(func(c *gin.Context) {
c.Next()
}),
timeout.WithResponse(testResponse),
)
}

func main() {
r := gin.New()
r.Use(extendedTimeoutMiddleware())
r.GET("/extended", func(c *gin.Context) {
time.Sleep(800 * time.Millisecond)
c.Status(http.StatusOK)
})

if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
```
39 changes: 39 additions & 0 deletions _example/example03/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"log"
"net/http"
"time"

"github.com/gin-contrib/timeout"
"github.com/gin-gonic/gin"
)

func testResponse(c *gin.Context) {
c.String(http.StatusRequestTimeout, "timeout")
}

func extendedTimeoutMiddleware() gin.HandlerFunc {
return timeout.New(
timeout.WithTimeout(200*time.Millisecond), // Default timeout on all routes
timeout.WithExtendedTimeout(1000*time.Millisecond), // Extended timeout on pattern based routes
timeout.WithExtendedPaths([]string{"/ext.*"}), // List of patterns to allow extended timeouts
timeout.WithHandler(func(c *gin.Context) {
c.Next()
}),
timeout.WithResponse(testResponse),
)
}

func main() {
r := gin.New()
r.Use(extendedTimeoutMiddleware())
r.GET("/extended", func(c *gin.Context) {
time.Sleep(800 * time.Millisecond)
c.Status(http.StatusOK)
})

if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
40 changes: 37 additions & 3 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package timeout

import (
"net/http"
"regexp"
"time"

"github.com/gin-gonic/gin"
Expand All @@ -17,6 +18,20 @@ func WithTimeout(timeout time.Duration) Option {
}
}

// WithExtendedTimeout set extended paths timeout
func WithExtendedTimeout(extendedTimeout time.Duration) Option {
return func(t *Timeout) {
t.extendedTimeout = extendedTimeout
}
}

// WithExtendedPaths set extended paths
func WithExtendedPaths(extendedPaths []string) Option {
return func(t *Timeout) {
t.extendedPaths = extendedPaths
}
}

// WithHandler add gin handler
func WithHandler(h gin.HandlerFunc) Option {
return func(t *Timeout) {
Expand All @@ -35,9 +50,28 @@ func defaultResponse(c *gin.Context) {
c.String(http.StatusRequestTimeout, http.StatusText(http.StatusRequestTimeout))
}

// shouldExtendPathTimeout receiver matches the current path against the list of extensible timeouts.
// Not providing extended paths will not override the normal timeout duration.
func (t *Timeout) shouldExtendPathTimeout(c *gin.Context) bool {
for _, b := range t.extendedPaths {
matched, err := regexp.MatchString(b, c.Request.URL.Path)
if err != nil {
return false
}

if matched {
return true
}
}

return false
}

// Timeout struct
type Timeout struct {
timeout time.Duration
handler gin.HandlerFunc
response gin.HandlerFunc
timeout time.Duration
extendedTimeout time.Duration
extendedPaths []string
handler gin.HandlerFunc
response gin.HandlerFunc
}
14 changes: 10 additions & 4 deletions timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ const (
// New wraps a handler and aborts the process of the handler if the timeout is reached
func New(opts ...Option) gin.HandlerFunc {
t := &Timeout{
timeout: defaultTimeout,
handler: nil,
response: defaultResponse,
timeout: defaultTimeout,
extendedTimeout: defaultTimeout,
handler: nil,
response: defaultResponse,
}

// Loop through each option
Expand Down Expand Up @@ -46,6 +47,11 @@ func New(opts ...Option) gin.HandlerFunc {
c.Writer = tw
buffer.Reset()

timeout := t.timeout
if t.shouldExtendPathTimeout(c) {
timeout = t.extendedTimeout
}

go func() {
defer func() {
if p := recover(); p != nil {
Expand Down Expand Up @@ -77,7 +83,7 @@ func New(opts ...Option) gin.HandlerFunc {
tw.FreeBuffer()
bufPool.Put(buffer)

case <-time.After(t.timeout):
case <-time.After(timeout):
c.Abort()
tw.mu.Lock()
defer tw.mu.Unlock()
Expand Down
45 changes: 45 additions & 0 deletions timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,48 @@ func TestPanic(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "", w.Body.String())
}

func TestExtendedSuccess(t *testing.T) {
r := gin.New()
r.Use(New(
WithTimeout(50*time.Microsecond),
WithExtendedTimeout(250*time.Microsecond),
WithExtendedPaths([]string{"/ex.*"}),
WithHandler(emptySuccessResponse),
))

r.GET("/extended")

w := httptest.NewRecorder()

reqExFails, _ := http.NewRequestWithContext(context.Background(), "GET", "/extended", nil)
r.ServeHTTP(w, reqExFails)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "", w.Body.String())
}

func TestExtendedTimeout(t *testing.T) {
r := gin.New()
r.Use(New(
WithTimeout(50*time.Microsecond),
WithExtendedTimeout(200*time.Microsecond),
WithExtendedPaths([]string{"/extended"}),
WithHandler(extendedTimeout),
))

r.GET("/extended")

w := httptest.NewRecorder()

reqExFails, _ := http.NewRequestWithContext(context.Background(), "GET", "/extended", nil)
r.ServeHTTP(w, reqExFails)

assert.Equal(t, http.StatusRequestTimeout, w.Code)
assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String())
}

func extendedTimeout(ctx *gin.Context) {
// Let the route artificially timeout
time.Sleep(250 * time.Microsecond)
}