├── .github ├── dependabot.yml └── workflows │ ├── goreleaser.yml │ ├── trivy-scan.yml │ ├── codeql.yml │ └── go.yml ├── _example ├── example03 │ ├── concurrent_requests.sh │ ├── main.go │ ├── go.mod │ └── go.sum ├── example01 │ ├── main.go │ ├── go.mod │ └── go.sum ├── example02 │ ├── main.go │ ├── go.mod │ └── go.sum └── example04 │ ├── main.go │ ├── go.mod │ ├── README.md │ └── go.sum ├── .gitignore ├── .goreleaser.yaml ├── option.go ├── buffer_pool.go ├── LICENSE ├── option_test.go ├── .golangci.yml ├── buffer_pool_test.go ├── go.mod ├── writer.go ├── timeout.go ├── timeout_test.go ├── writer_test.go ├── go.sum └── README.md /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /_example/example03/concurrent_requests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make 5 concurrent requests to http://localhost:8000/long 4 | 5 | for i in {1..5} 6 | do 7 | curl -s http://localhost:8000/long & 8 | done 9 | 10 | wait 11 | echo "All 5 requests completed." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea -------------------------------------------------------------------------------- /_example/example01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-contrib/timeout" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func emptySuccessResponse(c *gin.Context) { 13 | time.Sleep(200 * time.Microsecond) 14 | c.String(http.StatusOK, "") 15 | } 16 | 17 | func main() { 18 | r := gin.New() 19 | 20 | r.GET("/", timeout.New( 21 | timeout.WithTimeout(100*time.Microsecond), 22 | ), 23 | emptySuccessResponse, 24 | ) 25 | 26 | // Listen and Server in 0.0.0.0:8080 27 | if err := r.Run(":8080"); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | 4 | changelog: 5 | use: github 6 | groups: 7 | - title: Features 8 | regexp: "^.*feat[(\\w)]*:+.*$" 9 | order: 0 10 | - title: "Bug fixes" 11 | regexp: "^.*fix[(\\w)]*:+.*$" 12 | order: 1 13 | - title: "Enhancements" 14 | regexp: "^.*chore[(\\w)]*:+.*$" 15 | order: 2 16 | - title: "Refactor" 17 | regexp: "^.*refactor[(\\w)]*:+.*$" 18 | order: 3 19 | - title: "Build process updates" 20 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 21 | order: 4 22 | - title: "Documentation updates" 23 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: Others 26 | order: 999 27 | -------------------------------------------------------------------------------- /_example/example02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-contrib/timeout" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func testResponse(c *gin.Context) { 13 | c.String(http.StatusRequestTimeout, "timeout") 14 | } 15 | 16 | func timeoutMiddleware() gin.HandlerFunc { 17 | return timeout.New( 18 | timeout.WithTimeout(500*time.Millisecond), 19 | timeout.WithResponse(testResponse), 20 | ) 21 | } 22 | 23 | func main() { 24 | r := gin.New() 25 | r.Use(timeoutMiddleware()) 26 | r.GET("/slow", func(c *gin.Context) { 27 | time.Sleep(800 * time.Millisecond) 28 | c.Status(http.StatusOK) 29 | }) 30 | if err := r.Run(":8080"); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Option for timeout 11 | type Option func(*Timeout) 12 | 13 | // WithTimeout set timeout 14 | func WithTimeout(timeout time.Duration) Option { 15 | return func(t *Timeout) { 16 | t.timeout = timeout 17 | } 18 | } 19 | 20 | // WithResponse add gin handler 21 | func WithResponse(h gin.HandlerFunc) Option { 22 | return func(t *Timeout) { 23 | t.response = h 24 | } 25 | } 26 | 27 | func defaultResponse(c *gin.Context) { 28 | c.String(http.StatusRequestTimeout, http.StatusText(http.StatusRequestTimeout)) 29 | } 30 | 31 | // Timeout struct 32 | type Timeout struct { 33 | timeout time.Duration 34 | response gin.HandlerFunc 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v6 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /_example/example03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-contrib/timeout" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func main() { 13 | r := gin.Default() 14 | 15 | r.Use(timeout.New( 16 | timeout.WithTimeout(100*time.Microsecond), 17 | ), func(c *gin.Context) { 18 | c.Next() 19 | 20 | if c.Writer.Status() == http.StatusRequestTimeout { 21 | slog.Error("request timeout") 22 | } 23 | }) 24 | 25 | r.GET("/long", func(c *gin.Context) { 26 | time.Sleep(10 * time.Second) 27 | c.String(http.StatusOK, "long time ago") 28 | }) 29 | 30 | s := &http.Server{ 31 | Addr: ":8000", 32 | Handler: r, 33 | ReadTimeout: 30 * time.Second, 34 | WriteTimeout: 30 * time.Second, 35 | ReadHeaderTimeout: time.Second * 5, 36 | } 37 | 38 | if err := s.ListenAndServe(); err != nil { 39 | slog.Error("ListenAndServe failed", "err", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /buffer_pool.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // BufferPool represents a pool of buffers. 9 | // It uses sync.Pool to manage the reuse of buffers, reducing memory allocation and garbage collection overhead. 10 | type BufferPool struct { 11 | pool sync.Pool 12 | } 13 | 14 | // Get returns a buffer from the buffer pool. 15 | // If the pool is empty, a new buffer is created and returned. 16 | // This method ensures the reuse of buffers, improving performance. 17 | func (p *BufferPool) Get() *bytes.Buffer { 18 | buf := p.pool.Get() 19 | if buf == nil { 20 | // If there are no available buffers in the pool, create a new one 21 | return &bytes.Buffer{} 22 | } 23 | // Convert the retrieved buffer to *bytes.Buffer type and return it 24 | return buf.(*bytes.Buffer) 25 | } 26 | 27 | // Put adds a buffer back to the pool. 28 | // This method allows the buffer to be reused in the future, reducing the number of memory allocations. 29 | func (p *BufferPool) Put(buf *bytes.Buffer) { 30 | p.pool.Put(buf) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gin-Gonic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestOptions(t *testing.T) { 14 | t.Parallel() 15 | 16 | timeout := &Timeout{} 17 | customTimeout := 5 * time.Second 18 | customResponse := func(c *gin.Context) { 19 | c.String(http.StatusGatewayTimeout, "test response") 20 | } 21 | 22 | // Apply options 23 | WithTimeout(customTimeout)(timeout) 24 | WithResponse(customResponse)(timeout) 25 | 26 | // Assertions 27 | assert.Equal(t, customTimeout, timeout.timeout) 28 | assert.NotNil(t, timeout.response) 29 | 30 | // To fully verify the handler, we can execute it and check the response. 31 | w := httptest.NewRecorder() 32 | c, _ := gin.CreateTestContext(w) 33 | timeout.response(c) 34 | assert.Equal(t, http.StatusGatewayTimeout, w.Code) 35 | assert.Equal(t, "test response", w.Body.String()) 36 | } 37 | 38 | func TestDefaultResponse(t *testing.T) { 39 | t.Parallel() 40 | 41 | w := httptest.NewRecorder() 42 | c, _ := gin.CreateTestContext(w) 43 | 44 | defaultResponse(c) 45 | 46 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 47 | assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String()) 48 | } 49 | -------------------------------------------------------------------------------- /_example/example04/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-contrib/timeout" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func testResponse(c *gin.Context) { 13 | c.String(http.StatusRequestTimeout, "timeout") 14 | } 15 | 16 | // custom middleware straight from example 17 | func timeoutMiddleware() gin.HandlerFunc { 18 | return timeout.New( 19 | timeout.WithTimeout(500*time.Millisecond), 20 | timeout.WithResponse(testResponse), 21 | ) 22 | } 23 | 24 | // simple middleware to always throw a 401 25 | func authMiddleware() gin.HandlerFunc { 26 | return func(c *gin.Context) { 27 | debug := c.Query("debug") 28 | if debug != "true" { 29 | c.Next() 30 | return 31 | } 32 | c.AbortWithStatus(401) 33 | } 34 | } 35 | 36 | func main() { 37 | r := gin.New() 38 | 39 | // middleware 40 | r.Use(gin.Logger()) 41 | r.Use(timeoutMiddleware()) // 1. timeout middleware 42 | r.Use(authMiddleware()) // 2. auth middleware 43 | r.Use( 44 | gin.Recovery(), 45 | ) // recommend to use this middleware to recover from any panics in the handlers. 46 | 47 | r.GET("/", func(c *gin.Context) { 48 | time.Sleep(1000 * time.Millisecond) 49 | c.String(http.StatusOK, "Hello world!") 50 | }) 51 | if err := r.Run(":8080"); err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - depguard 7 | - dogsled 8 | - dupl 9 | - errcheck 10 | - exhaustive 11 | - gochecknoinits 12 | - goconst 13 | - gocritic 14 | - gocyclo 15 | - goprintffuncname 16 | - gosec 17 | - govet 18 | - ineffassign 19 | - lll 20 | - misspell 21 | - nakedret 22 | - noctx 23 | - nolintlint 24 | - rowserrcheck 25 | - staticcheck 26 | - unconvert 27 | - unparam 28 | - unused 29 | - whitespace 30 | settings: 31 | depguard: 32 | rules: 33 | Main: 34 | files: 35 | - $all 36 | - "!$test" 37 | allow: 38 | - $gostd 39 | - github.com/gin-gonic/gin 40 | Test: 41 | files: 42 | - $test 43 | allow: 44 | - $gostd 45 | - github.com/gin-gonic/gin 46 | - github.com/stretchr/testify 47 | exclusions: 48 | generated: lax 49 | presets: 50 | - comments 51 | - common-false-positives 52 | - legacy 53 | - std-error-handling 54 | paths: 55 | - third_party$ 56 | - builtin$ 57 | - examples$ 58 | formatters: 59 | enable: 60 | - gofmt 61 | - gofumpt 62 | - goimports 63 | - golines 64 | exclusions: 65 | generated: lax 66 | paths: 67 | - third_party$ 68 | - builtin$ 69 | - examples$ 70 | -------------------------------------------------------------------------------- /_example/example01/go.mod: -------------------------------------------------------------------------------- 1 | module example01 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/gin-contrib/timeout v1.0.2 7 | github.com/gin-gonic/gin v1.10.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.3 // indirect 12 | github.com/bytedance/sonic/loader v0.3.0 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 15 | github.com/gin-contrib/sse v1.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.27.0 // indirect 19 | github.com/goccy/go-json v0.10.5 // indirect 20 | github.com/json-iterator/go v1.1.12 // indirect 21 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/mattn/go-isatty v0.0.20 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.3.0 // indirect 29 | golang.org/x/arch v0.19.0 // indirect 30 | golang.org/x/crypto v0.45.0 // indirect 31 | golang.org/x/net v0.47.0 // indirect 32 | golang.org/x/sys v0.38.0 // indirect 33 | golang.org/x/text v0.31.0 // indirect 34 | google.golang.org/protobuf v1.36.6 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | 38 | replace github.com/gin-contrib/timeout => ../../ 39 | -------------------------------------------------------------------------------- /_example/example03/go.mod: -------------------------------------------------------------------------------- 1 | module example03 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/gin-contrib/timeout v1.0.2 7 | github.com/gin-gonic/gin v1.10.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.3 // indirect 12 | github.com/bytedance/sonic/loader v0.3.0 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 15 | github.com/gin-contrib/sse v1.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.27.0 // indirect 19 | github.com/goccy/go-json v0.10.5 // indirect 20 | github.com/json-iterator/go v1.1.12 // indirect 21 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/mattn/go-isatty v0.0.20 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.3.0 // indirect 29 | golang.org/x/arch v0.19.0 // indirect 30 | golang.org/x/crypto v0.45.0 // indirect 31 | golang.org/x/net v0.47.0 // indirect 32 | golang.org/x/sys v0.38.0 // indirect 33 | golang.org/x/text v0.31.0 // indirect 34 | google.golang.org/protobuf v1.36.6 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | 38 | replace github.com/gin-contrib/timeout => ../../ 39 | -------------------------------------------------------------------------------- /_example/example04/go.mod: -------------------------------------------------------------------------------- 1 | module example04 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/gin-contrib/timeout v1.0.2 7 | github.com/gin-gonic/gin v1.10.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.3 // indirect 12 | github.com/bytedance/sonic/loader v0.3.0 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 15 | github.com/gin-contrib/sse v1.1.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.27.0 // indirect 19 | github.com/goccy/go-json v0.10.5 // indirect 20 | github.com/json-iterator/go v1.1.12 // indirect 21 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 22 | github.com/leodido/go-urn v1.4.0 // indirect 23 | github.com/mattn/go-isatty v0.0.20 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.3.0 // indirect 29 | golang.org/x/arch v0.19.0 // indirect 30 | golang.org/x/crypto v0.45.0 // indirect 31 | golang.org/x/net v0.47.0 // indirect 32 | golang.org/x/sys v0.38.0 // indirect 33 | golang.org/x/text v0.31.0 // indirect 34 | google.golang.org/protobuf v1.36.6 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | 38 | replace github.com/gin-contrib/timeout => ../../ 39 | -------------------------------------------------------------------------------- /buffer_pool_test.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBufferPool(t *testing.T) { 11 | t.Parallel() 12 | 13 | pool := &BufferPool{} 14 | buf := pool.Get() 15 | assert.NotNil(t, buf) 16 | 17 | pool.Put(buf) 18 | buf2 := pool.Get() 19 | assert.NotNil(t, buf2) 20 | assert.Same(t, buf, buf2) 21 | } 22 | 23 | func TestBufferPool_Concurrent(t *testing.T) { 24 | t.Parallel() 25 | 26 | pool := &BufferPool{} 27 | numGoroutines := 50 28 | numGetsPerGoRoutine := 100 29 | 30 | var wg sync.WaitGroup 31 | wg.Add(numGoroutines) 32 | 33 | for i := 0; i < numGoroutines; i++ { 34 | go func() { 35 | defer wg.Done() 36 | for j := 0; j < numGetsPerGoRoutine; j++ { 37 | buf := pool.Get() 38 | assert.NotNil(t, buf) 39 | assert.Equal(t, 0, buf.Len(), "buffer should be empty") 40 | 41 | buf.WriteString("test") 42 | buf.Reset() 43 | pool.Put(buf) 44 | } 45 | }() 46 | } 47 | 48 | wg.Wait() 49 | } 50 | 51 | func TestBufferPool_NoReset(t *testing.T) { 52 | t.Parallel() 53 | 54 | // This test demonstrates that it is the responsibility of the 55 | // caller to reset the buffer before putting it back into the pool. 56 | pool := &BufferPool{} 57 | 58 | // Get a buffer, write to it, and put it back without resetting. 59 | buf := pool.Get() 60 | buf.WriteString("hello") 61 | pool.Put(buf) 62 | 63 | // Get the buffer again and check if the old content is still there. 64 | buf2 := pool.Get() 65 | assert.Same(t, buf, buf2) 66 | assert.Equal(t, "hello", buf2.String()) 67 | } 68 | -------------------------------------------------------------------------------- /_example/example02/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/gin-contrib/timeout v0.0.3 9 | github.com/gin-gonic/gin v1.10.1 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.13.3 // indirect 14 | github.com/bytedance/sonic/loader v0.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.5 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 17 | github.com/gin-contrib/sse v1.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.27.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.3.0 // indirect 31 | golang.org/x/arch v0.19.0 // indirect 32 | golang.org/x/crypto v0.45.0 // indirect 33 | golang.org/x/net v0.47.0 // indirect 34 | golang.org/x/sys v0.38.0 // indirect 35 | golang.org/x/text v0.31.0 // indirect 36 | google.golang.org/protobuf v1.36.6 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | 40 | replace github.com/gin-contrib/timeout => ../../ 41 | -------------------------------------------------------------------------------- /.github/workflows/trivy-scan.yml: -------------------------------------------------------------------------------- 1 | name: Trivy Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | # Run daily at 00:00 UTC 12 | - cron: '0 0 * * *' 13 | workflow_dispatch: # Allow manual trigger 14 | 15 | permissions: 16 | contents: read 17 | security-events: write # Required for uploading SARIF results 18 | 19 | jobs: 20 | trivy-scan: 21 | name: Trivy Security Scan 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v6 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Run Trivy vulnerability scanner (source code) 30 | uses: aquasecurity/trivy-action@0.33.1 31 | with: 32 | scan-type: 'fs' 33 | scan-ref: '.' 34 | scanners: 'vuln,secret,misconfig' 35 | format: 'sarif' 36 | output: 'trivy-results.sarif' 37 | severity: 'CRITICAL,HIGH,MEDIUM' 38 | ignore-unfixed: true 39 | 40 | - name: Upload Trivy results to GitHub Security tab 41 | uses: github/codeql-action/upload-sarif@v4 42 | if: always() 43 | with: 44 | sarif_file: 'trivy-results.sarif' 45 | 46 | - name: Run Trivy scanner (table output for logs) 47 | uses: aquasecurity/trivy-action@0.33.1 48 | if: always() 49 | with: 50 | scan-type: 'fs' 51 | scan-ref: '.' 52 | scanners: 'vuln,secret,misconfig' 53 | format: 'table' 54 | severity: 'CRITICAL,HIGH,MEDIUM' 55 | ignore-unfixed: true 56 | exit-code: '1' 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-contrib/timeout 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.11.0 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.14.0 // indirect 12 | github.com/bytedance/sonic/loader v0.3.0 // indirect 13 | github.com/cloudwego/base64x v0.1.6 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 16 | github.com/gin-contrib/sse v1.1.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.27.0 // indirect 20 | github.com/goccy/go-json v0.10.5 // indirect 21 | github.com/goccy/go-yaml v1.18.0 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/quic-go/qpack v0.6.0 // indirect 32 | github.com/quic-go/quic-go v0.57.1 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.3.0 // indirect 35 | golang.org/x/arch v0.20.0 // indirect 36 | golang.org/x/crypto v0.45.0 // indirect 37 | golang.org/x/net v0.47.0 // indirect 38 | golang.org/x/sys v0.38.0 // indirect 39 | golang.org/x/text v0.31.0 // indirect 40 | google.golang.org/protobuf v1.36.9 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v4 55 | -------------------------------------------------------------------------------- /_example/example04/README.md: -------------------------------------------------------------------------------- 1 | # Example 04: Gin Timeout & Admin Middleware Demo 2 | 3 | This example demonstrates the use of the [`timeout`](https://github.com/gin-contrib/timeout) middleware and a custom admin (auth) middleware in a Gin web server. 4 | 5 | ## How to Run 6 | 7 | ```bash 8 | cd _example/example04 9 | go run main.go 10 | ``` 11 | 12 | The server will start on [http://localhost:8080](http://localhost:8080). 13 | 14 | ## Middleware Stack 15 | 16 | 1. **Logger**: Logs all requests. 17 | 2. **Timeout**: Aborts requests taking longer than 500ms, returning HTTP 408 (Request Timeout) with body `timeout`. 18 | 3. **Auth Middleware**: If the query parameter `debug=true` is present, aborts with HTTP 401 (Unauthorized). 19 | 4. **Recovery**: Recovers from panics. 20 | 21 | ## Route 22 | 23 | - `GET /` 24 | Sleeps for 1 second, then responds with "Hello world!" (but will always timeout due to the 500ms limit). 25 | 26 | --- 27 | 28 | ## Testing 408 (Request Timeout) 29 | 30 | Any request to `/` will trigger the timeout middleware, since the handler sleeps for 1 second (exceeding the 500ms timeout). 31 | 32 | ```bash 33 | curl -i http://localhost:8080/ 34 | ``` 35 | 36 | **Expected response:** 37 | 38 | ```bash 39 | HTTP/1.1 408 Request Timeout 40 | Content-Type: text/plain; charset=utf-8 41 | 42 | timeout 43 | ``` 44 | 45 | ## Testing 401 (Unauthorized) 46 | 47 | To trigger the 401 response from the admin (auth) middleware, add the `debug=true` query parameter: 48 | 49 | ```bash 50 | curl -i "http://localhost:8080/?debug=true" 51 | ``` 52 | 53 | **Expected response:** 54 | 55 | ```bash 56 | HTTP/1.1 401 Unauthorized 57 | Content-Type: text/plain; charset=utf-8 58 | ``` 59 | 60 | > **Note:** 61 | > Because the `/` handler always sleeps for 1 second, the timeout middleware (408) will usually trigger before the 401. 62 | > To reliably test the 401, you can temporarily comment out the `time.Sleep(1000 * time.Millisecond)` line in `main.go` and restart the server. 63 | 64 | --- 65 | 66 | ## Summary 67 | 68 | - **408**: Triggered by slow handler (default behavior). 69 | - **401**: Triggered by `?debug=true` (if handler is fast enough). 70 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v6 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v9 27 | with: 28 | version: v2.6 29 | args: --verbose 30 | test: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | go: [1.24, 1.25] 35 | race: [true, false] 36 | include: 37 | - os: ubuntu-latest 38 | go-build: ~/.cache/go-build 39 | name: "${{ matrix.os }} @ Go ${{ matrix.go }} (race: ${{ matrix.race }})" 40 | runs-on: ${{ matrix.os }} 41 | env: 42 | GO111MODULE: on 43 | GOPROXY: https://proxy.golang.org 44 | steps: 45 | - name: Set up Go ${{ matrix.go }} 46 | uses: actions/setup-go@v6 47 | with: 48 | go-version: ${{ matrix.go }} 49 | 50 | - name: Checkout Code 51 | uses: actions/checkout@v6 52 | with: 53 | ref: ${{ github.ref }} 54 | 55 | - uses: actions/cache@v5 56 | with: 57 | path: | 58 | ${{ matrix.go-build }} 59 | ~/go/pkg/mod 60 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 61 | restore-keys: | 62 | ${{ runner.os }}-go- 63 | - name: Run Tests 64 | run: | 65 | if [[ "${{ matrix.race }}" == "true" ]]; then 66 | go test -race -v -covermode=atomic -coverprofile=coverage.out 67 | else 68 | go test -v -covermode=atomic -coverprofile=coverage.out 69 | fi 70 | 71 | - name: Upload coverage to Codecov 72 | uses: codecov/codecov-action@v5 73 | with: 74 | flags: "${{ matrix.os }},go-${{ matrix.go }},race-${{ matrix.race }}" 75 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // Writer is a writer with memory buffer 13 | type Writer struct { 14 | gin.ResponseWriter 15 | body *bytes.Buffer 16 | headers http.Header 17 | mu sync.Mutex 18 | timeout bool 19 | wroteHeaders bool 20 | code int 21 | size int 22 | } 23 | 24 | // NewWriter will return a timeout.Writer pointer 25 | func NewWriter(w gin.ResponseWriter, buf *bytes.Buffer) *Writer { 26 | return &Writer{ResponseWriter: w, body: buf, headers: make(http.Header)} 27 | } 28 | 29 | // WriteHeaderNow the reason why we override this func is: 30 | // once calling the func WriteHeaderNow() of based gin.ResponseWriter, 31 | // this Writer can no longer apply the cached headers to the based 32 | // gin.ResponseWriter. see test case `TestWriter_WriteHeaderNow` for details. 33 | func (w *Writer) WriteHeaderNow() { 34 | if !w.wroteHeaders { 35 | if w.code == 0 { 36 | w.code = http.StatusOK 37 | } 38 | 39 | // Copy headers from our cache to the underlying ResponseWriter 40 | dst := w.ResponseWriter.Header() 41 | for k, vv := range w.headers { 42 | dst[k] = vv 43 | } 44 | 45 | w.WriteHeader(w.code) 46 | } 47 | } 48 | 49 | // WriteHeader sends an HTTP response header with the provided status code. 50 | // If the response writer has already written headers or if a timeout has occurred, 51 | // this method does nothing. 52 | func (w *Writer) WriteHeader(code int) { 53 | w.mu.Lock() 54 | defer w.mu.Unlock() 55 | 56 | if w.timeout || w.wroteHeaders { 57 | return 58 | } 59 | 60 | // gin is using -1 to skip writing the status code 61 | // see https://github.com/gin-gonic/gin/blob/a0acf1df2814fcd828cb2d7128f2f4e2136d3fac/response_writer.go#L61 62 | if code == -1 { 63 | return 64 | } 65 | 66 | checkWriteHeaderCode(code) 67 | 68 | // Copy headers from our cache to the underlying ResponseWriter 69 | dst := w.ResponseWriter.Header() 70 | for k, vv := range w.headers { 71 | dst[k] = vv 72 | } 73 | 74 | w.writeHeader(code) 75 | w.ResponseWriter.WriteHeader(code) 76 | } 77 | 78 | func (w *Writer) writeHeader(code int) { 79 | w.wroteHeaders = true 80 | w.code = code 81 | } 82 | 83 | // Header will get response headers 84 | func (w *Writer) Header() http.Header { 85 | return w.headers 86 | } 87 | 88 | // Write will write data to response body 89 | func (w *Writer) Write(data []byte) (int, error) { 90 | w.mu.Lock() 91 | defer w.mu.Unlock() 92 | 93 | if w.timeout || w.body == nil { 94 | return 0, nil 95 | } 96 | 97 | n, err := w.body.Write(data) 98 | w.size += n 99 | 100 | return n, err 101 | } 102 | 103 | // WriteString will write string to response body 104 | func (w *Writer) WriteString(s string) (int, error) { 105 | n, err := w.Write([]byte(s)) 106 | w.size += n 107 | return n, err 108 | } 109 | 110 | func (w *Writer) Size() int { 111 | return w.size 112 | } 113 | 114 | // FreeBuffer will release buffer pointer 115 | func (w *Writer) FreeBuffer() { 116 | // if not reset body,old bytes will put in bufPool 117 | w.body.Reset() 118 | w.size = -1 119 | w.body = nil 120 | } 121 | 122 | // Status we must override Status func here, 123 | // or the http status code returned by gin.Context.Writer.Status() 124 | // will always be 200 in other custom gin middlewares. 125 | func (w *Writer) Status() int { 126 | if w.code == 0 || w.timeout { 127 | return w.ResponseWriter.Status() 128 | } 129 | return w.code 130 | } 131 | 132 | func checkWriteHeaderCode(code int) { 133 | if code < 100 || code > 999 { 134 | panic(fmt.Sprintf("invalid http status code: %d", code)) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /timeout.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime/debug" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | var bufPool *BufferPool 13 | 14 | const ( 15 | defaultTimeout = 5 * time.Second 16 | ) 17 | 18 | // panicChan transmits both the panic value and the stack trace. 19 | type panicInfo struct { 20 | Value interface{} 21 | Stack []byte 22 | } 23 | 24 | // New wraps a handler and aborts the process of the handler if the timeout is reached 25 | func New(opts ...Option) gin.HandlerFunc { 26 | t := &Timeout{ 27 | timeout: defaultTimeout, 28 | response: defaultResponse, 29 | } 30 | 31 | // Apply each option to the Timeout instance 32 | for _, opt := range opts { 33 | if opt == nil { 34 | panic("timeout Option must not be nil") 35 | } 36 | 37 | // Call the option to configure the Timeout instance 38 | opt(t) 39 | } 40 | 41 | // Initialize the buffer pool for response writers. 42 | bufPool = &BufferPool{} 43 | 44 | return func(c *gin.Context) { 45 | // Swap the response writer with a buffered writer. 46 | w := c.Writer 47 | buffer := bufPool.Get() 48 | tw := NewWriter(w, buffer) 49 | c.Writer = tw 50 | buffer.Reset() 51 | 52 | // Create a copy of the context before starting the goroutine to avoid data race 53 | cCopy := c.Copy() 54 | // Set the copied context's writer to our timeout writer to ensure proper buffering 55 | cCopy.Writer = tw 56 | 57 | // Channel to signal handler completion. 58 | finish := make(chan struct{}, 1) 59 | panicChan := make(chan panicInfo, 1) 60 | 61 | // Run the handler in a separate goroutine to enforce timeout and catch panics. 62 | go func() { 63 | defer func() { 64 | if p := recover(); p != nil { 65 | // Capture both the panic value and the stack trace. 66 | panicChan <- panicInfo{ 67 | Value: p, 68 | Stack: debug.Stack(), 69 | } 70 | } 71 | }() 72 | // Use the copied context to avoid data race when running handler in a goroutine. 73 | c.Next() 74 | finish <- struct{}{} 75 | }() 76 | 77 | select { 78 | case pi := <-panicChan: 79 | // Handler panicked: free buffer, restore writer, and print stack trace if in debug mode. 80 | tw.FreeBuffer() 81 | c.Writer = w 82 | // If in debug mode, write error and stack trace to response for easier debugging. 83 | if gin.IsDebugging() { 84 | // Add the panic error to Gin's error list and write 500 status and stack trace to response. 85 | // Check the error return value of c.Error to satisfy errcheck linter. 86 | _ = c.Error(fmt.Errorf("%v", pi.Value)) 87 | c.Writer.WriteHeader(http.StatusInternalServerError) 88 | // Use fmt.Fprintf instead of Write([]byte(fmt.Sprintf(...))) to satisfy staticcheck. 89 | _, _ = fmt.Fprintf(c.Writer, "panic caught: %v\n", pi.Value) 90 | _, _ = c.Writer.Write([]byte("Panic stack trace:\n")) 91 | _, _ = c.Writer.Write(pi.Stack) 92 | return 93 | } 94 | // In non-debug mode, re-throw the original panic value to be handled by the upper middleware. 95 | panic(pi.Value) 96 | case <-finish: 97 | // Handler finished successfully: flush buffer to response. 98 | tw.mu.Lock() 99 | defer tw.mu.Unlock() 100 | dst := tw.ResponseWriter.Header() 101 | for k, vv := range tw.Header() { 102 | dst[k] = vv 103 | } 104 | 105 | // Write the status code if it was set, otherwise use 200 106 | if tw.code != 0 { 107 | tw.ResponseWriter.WriteHeader(tw.code) 108 | } 109 | 110 | // Only write content if there's any 111 | if buffer.Len() > 0 { 112 | if _, err := tw.ResponseWriter.Write(buffer.Bytes()); err != nil { 113 | panic(err) 114 | } 115 | } 116 | tw.FreeBuffer() 117 | bufPool.Put(buffer) 118 | 119 | case <-time.After(t.timeout): 120 | tw.mu.Lock() 121 | // Handler timed out: set timeout flag and clean up 122 | tw.timeout = true 123 | tw.FreeBuffer() 124 | bufPool.Put(buffer) 125 | tw.mu.Unlock() 126 | 127 | // Create a fresh context for the timeout response 128 | // Important: check if headers were already written 129 | timeoutCtx := c.Copy() 130 | timeoutCtx.Writer = w 131 | 132 | // Only write timeout response if headers haven't been written to original writer 133 | if !w.Written() { 134 | t.response(timeoutCtx) 135 | } 136 | // Abort the context to prevent further middleware execution after timeout 137 | c.AbortWithStatus(http.StatusRequestTimeout) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /timeout_test.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func emptySuccessResponse(c *gin.Context) { 16 | time.Sleep(200 * time.Microsecond) 17 | c.String(http.StatusOK, "") 18 | } 19 | 20 | func TestTimeout(t *testing.T) { 21 | r := gin.New() 22 | r.GET("/", New( 23 | WithTimeout(50*time.Microsecond), 24 | ), 25 | emptySuccessResponse, 26 | ) 27 | 28 | w := httptest.NewRecorder() 29 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 30 | r.ServeHTTP(w, req) 31 | 32 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 33 | assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String()) 34 | } 35 | 36 | func TestTimeoutWithUse(t *testing.T) { 37 | r := gin.New() 38 | r.Use(New( 39 | WithTimeout(50 * time.Microsecond), 40 | )) 41 | r.GET("/", emptySuccessResponse) 42 | 43 | w := httptest.NewRecorder() 44 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 45 | r.ServeHTTP(w, req) 46 | 47 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 48 | assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String()) 49 | } 50 | 51 | func TestWithoutTimeout(t *testing.T) { 52 | r := gin.New() 53 | r.GET("/", New( 54 | WithTimeout(-1*time.Microsecond), 55 | ), 56 | emptySuccessResponse, 57 | ) 58 | 59 | w := httptest.NewRecorder() 60 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 61 | r.ServeHTTP(w, req) 62 | 63 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 64 | assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String()) 65 | } 66 | 67 | func testResponse(c *gin.Context) { 68 | c.String(http.StatusRequestTimeout, "test response") 69 | } 70 | 71 | func TestCustomResponse(t *testing.T) { 72 | r := gin.New() 73 | r.GET("/", New( 74 | WithTimeout(100*time.Microsecond), 75 | WithResponse(testResponse), 76 | ), 77 | emptySuccessResponse, 78 | ) 79 | 80 | w := httptest.NewRecorder() 81 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 82 | r.ServeHTTP(w, req) 83 | 84 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 85 | assert.Equal(t, "test response", w.Body.String()) 86 | } 87 | 88 | func emptySuccessResponse2(c *gin.Context) { 89 | time.Sleep(50 * time.Microsecond) 90 | c.String(http.StatusOK, "") 91 | } 92 | 93 | func TestSuccess(t *testing.T) { 94 | r := gin.New() 95 | r.GET("/", New( 96 | WithTimeout(1*time.Second), 97 | WithResponse(testResponse), 98 | ), 99 | emptySuccessResponse2, 100 | ) 101 | 102 | w := httptest.NewRecorder() 103 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 104 | r.ServeHTTP(w, req) 105 | 106 | assert.Equal(t, http.StatusOK, w.Code) 107 | assert.Equal(t, "", w.Body.String()) 108 | } 109 | 110 | func TestLargeResponse(t *testing.T) { 111 | r := gin.New() 112 | r.GET("/slow", New( 113 | WithTimeout(1*time.Second), 114 | WithResponse(func(c *gin.Context) { 115 | c.String(http.StatusRequestTimeout, `{"error": "timeout error"}`) 116 | }), 117 | ), 118 | func(c *gin.Context) { 119 | time.Sleep(2 * time.Second) // wait almost same as timeout 120 | c.String(http.StatusBadRequest, `{"error": "handler error"}`) 121 | }, 122 | ) 123 | 124 | wg := sync.WaitGroup{} 125 | for i := 0; i < 10; i++ { 126 | wg.Add(1) 127 | go func() { 128 | defer wg.Done() 129 | w := httptest.NewRecorder() 130 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/slow", nil) 131 | r.ServeHTTP(w, req) 132 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 133 | assert.Equal(t, `{"error": "timeout error"}`, w.Body.String()) 134 | }() 135 | } 136 | wg.Wait() 137 | } 138 | 139 | /* 140 | Test to ensure no further middleware is executed after timeout (covers c.Next() removal) 141 | This test verifies that after a timeout occurs, no subsequent middleware is executed. 142 | */ 143 | func TestNoNextAfterTimeout(t *testing.T) { 144 | r := gin.New() 145 | called := false 146 | r.Use(New( 147 | WithTimeout(50*time.Millisecond), 148 | ), 149 | func(c *gin.Context) { 150 | time.Sleep(100 * time.Millisecond) 151 | c.String(http.StatusOK, "should not reach") 152 | }, 153 | ) 154 | r.Use(func(c *gin.Context) { 155 | called = true 156 | }) 157 | 158 | w := httptest.NewRecorder() 159 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 160 | r.ServeHTTP(w, req) 161 | 162 | assert.Equal(t, http.StatusRequestTimeout, w.Code) 163 | assert.False(t, called, "next middleware should not be called after timeout") 164 | } 165 | 166 | /* 167 | TestTimeoutPanic: verifies the behavior when a panic occurs inside a handler wrapped by the timeout middleware. 168 | This test ensures that a panic in the handler is caught by CustomRecovery and returns a 500 status code 169 | with the panic message. 170 | */ 171 | func TestTimeoutPanic(t *testing.T) { 172 | r := gin.New() 173 | // Use CustomRecovery to catch panics and return a custom error message. 174 | r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { 175 | c.String(http.StatusInternalServerError, "panic caught: %v", recovered) 176 | })) 177 | 178 | // Register the timeout middleware; the handler will panic. 179 | r.GET("/panic", New( 180 | WithTimeout(100*time.Millisecond), 181 | ), 182 | func(c *gin.Context) { 183 | panic("timeout panic test") 184 | }, 185 | ) 186 | 187 | w := httptest.NewRecorder() 188 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/panic", nil) 189 | r.ServeHTTP(w, req) 190 | 191 | // Verify the response status code and body. 192 | assert.Equal(t, http.StatusInternalServerError, w.Code) 193 | assert.Contains(t, w.Body.String(), "panic caught: timeout panic test") 194 | } 195 | -------------------------------------------------------------------------------- /_example/example01/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 2 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 5 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 13 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 14 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 15 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 16 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 17 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 25 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 26 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 27 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 35 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 59 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 61 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 62 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 63 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 64 | golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= 65 | golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 66 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 67 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 68 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 69 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 72 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 73 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 74 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 75 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 76 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 83 | -------------------------------------------------------------------------------- /_example/example02/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 2 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 5 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 13 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 14 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 15 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 16 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 17 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 25 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 26 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 27 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 35 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 59 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 61 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 62 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 63 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 64 | golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= 65 | golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 66 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 67 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 68 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 69 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 72 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 73 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 74 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 75 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 76 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 83 | -------------------------------------------------------------------------------- /_example/example03/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 2 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 5 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 13 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 14 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 15 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 16 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 17 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 25 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 26 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 27 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 35 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 59 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 61 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 62 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 63 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 64 | golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= 65 | golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 66 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 67 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 68 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 69 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 72 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 73 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 74 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 75 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 76 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 83 | -------------------------------------------------------------------------------- /_example/example04/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 2 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 5 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 13 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 14 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 15 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 16 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 17 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 25 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 26 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 27 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 35 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 59 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 61 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 62 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 63 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 64 | golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= 65 | golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 66 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 67 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 68 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 69 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 72 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 73 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 74 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 75 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 76 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 83 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package timeout 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "strconv" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestWriteHeader(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | code int 21 | }{ 22 | { 23 | name: "code less than 100", 24 | code: 99, 25 | }, 26 | { 27 | name: "code greater than 999", 28 | code: 1000, 29 | }, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | writer := Writer{} 34 | errmsg := fmt.Sprintf("invalid http status code: %d", tt.code) 35 | assert.PanicsWithValue(t, errmsg, func() { 36 | writer.WriteHeader(tt.code) 37 | }) 38 | }) 39 | } 40 | } 41 | 42 | func TestWriteHeader_SkipMinusOne(t *testing.T) { 43 | code := -1 44 | 45 | writer := Writer{} 46 | assert.NotPanics(t, func() { 47 | writer.WriteHeader(code) 48 | assert.False(t, writer.wroteHeaders) 49 | }) 50 | } 51 | 52 | func TestWriter_Status(t *testing.T) { 53 | r := gin.New() 54 | 55 | r.Use(New( 56 | WithTimeout(1*time.Second), 57 | WithResponse(testResponse), 58 | )) 59 | 60 | r.Use(func(c *gin.Context) { 61 | c.Next() 62 | statusInMW := c.Writer.Status() 63 | c.Request.Header.Set("X-Status-Code-MW-Set", strconv.Itoa(statusInMW)) 64 | t.Logf( 65 | "[%s] %s %s %d\n", 66 | time.Now().Format(time.RFC3339), 67 | c.Request.Method, 68 | c.Request.URL, 69 | statusInMW, 70 | ) 71 | }) 72 | 73 | r.GET("/test", func(c *gin.Context) { 74 | c.Writer.WriteHeader(http.StatusInternalServerError) 75 | }) 76 | 77 | w := httptest.NewRecorder() 78 | req := httptest.NewRequest(http.MethodGet, "/test", nil) 79 | 80 | r.ServeHTTP(w, req) 81 | 82 | assert.Equal(t, http.StatusInternalServerError, w.Code) 83 | assert.Equal( 84 | t, 85 | strconv.Itoa(http.StatusInternalServerError), 86 | req.Header.Get("X-Status-Code-MW-Set"), 87 | ) 88 | } 89 | 90 | // testNew is a copy of New() with a small change to the timeoutHandler() function. 91 | // ref: https://github.com/gin-contrib/timeout/issues/31 92 | func testNew(duration time.Duration) gin.HandlerFunc { 93 | return New( 94 | WithTimeout(duration), 95 | WithResponse(timeoutHandler()), 96 | ) 97 | } 98 | 99 | // timeoutHandler returns a handler that returns a 504 Gateway Timeout error. 100 | func timeoutHandler() gin.HandlerFunc { 101 | gatewayTimeoutErr := struct { 102 | Error string `json:"error"` 103 | }{ 104 | Error: "Timed out.", 105 | } 106 | 107 | return func(c *gin.Context) { 108 | log.Printf("request timed out: [method=%s,path=%s]", 109 | c.Request.Method, c.Request.URL.Path) 110 | c.JSON(http.StatusGatewayTimeout, gatewayTimeoutErr) 111 | } 112 | } 113 | 114 | // TestHTTPStatusCode tests the HTTP status code of the response. 115 | func TestHTTPStatusCode(t *testing.T) { 116 | gin.SetMode(gin.ReleaseMode) 117 | 118 | type testCase struct { 119 | Name string 120 | Method string 121 | Path string 122 | ExpStatusCode int 123 | Handler gin.HandlerFunc 124 | } 125 | 126 | var ( 127 | cases = []testCase{ 128 | { 129 | Name: "Plain text (200)", 130 | Method: http.MethodGet, 131 | Path: "/me", 132 | ExpStatusCode: http.StatusOK, 133 | Handler: func(ctx *gin.Context) { 134 | ctx.String(http.StatusOK, "I'm text!") 135 | }, 136 | }, 137 | { 138 | Name: "Plain text (201)", 139 | Method: http.MethodGet, 140 | Path: "/me", 141 | ExpStatusCode: http.StatusCreated, 142 | Handler: func(ctx *gin.Context) { 143 | ctx.String(http.StatusCreated, "I'm created!") 144 | }, 145 | }, 146 | { 147 | Name: "Plain text (204)", 148 | Method: http.MethodGet, 149 | Path: "/me", 150 | ExpStatusCode: http.StatusNoContent, 151 | Handler: func(ctx *gin.Context) { 152 | ctx.String(http.StatusNoContent, "") 153 | }, 154 | }, 155 | { 156 | Name: "Plain text (400)", 157 | Method: http.MethodGet, 158 | Path: "/me", 159 | ExpStatusCode: http.StatusBadRequest, 160 | Handler: func(ctx *gin.Context) { 161 | ctx.String(http.StatusBadRequest, "") 162 | }, 163 | }, 164 | { 165 | Name: "JSON (200)", 166 | Method: http.MethodGet, 167 | Path: "/me", 168 | ExpStatusCode: http.StatusOK, 169 | Handler: func(ctx *gin.Context) { 170 | ctx.JSON(http.StatusOK, gin.H{"field": "value"}) 171 | }, 172 | }, 173 | { 174 | Name: "JSON (201)", 175 | Method: http.MethodGet, 176 | Path: "/me", 177 | ExpStatusCode: http.StatusCreated, 178 | Handler: func(ctx *gin.Context) { 179 | ctx.JSON(http.StatusCreated, gin.H{"field": "value"}) 180 | }, 181 | }, 182 | { 183 | Name: "JSON (204)", 184 | Method: http.MethodGet, 185 | Path: "/me", 186 | ExpStatusCode: http.StatusNoContent, 187 | Handler: func(ctx *gin.Context) { 188 | ctx.JSON(http.StatusNoContent, nil) 189 | }, 190 | }, 191 | { 192 | Name: "JSON (400)", 193 | Method: http.MethodGet, 194 | Path: "/me", 195 | ExpStatusCode: http.StatusBadRequest, 196 | Handler: func(ctx *gin.Context) { 197 | ctx.JSON(http.StatusBadRequest, nil) 198 | }, 199 | }, 200 | { 201 | Name: "No reply", 202 | Method: http.MethodGet, 203 | Path: "/me", 204 | ExpStatusCode: http.StatusOK, 205 | Handler: func(ctx *gin.Context) {}, 206 | }, 207 | } 208 | 209 | initCase = func(c testCase) (*http.Request, *httptest.ResponseRecorder) { 210 | return httptest.NewRequest(c.Method, c.Path, nil), httptest.NewRecorder() 211 | } 212 | ) 213 | 214 | for i := range cases { 215 | t.Run(cases[i].Name, func(tt *testing.T) { 216 | router := gin.Default() 217 | 218 | router.Use(testNew(1 * time.Second)) 219 | router.GET("/*root", cases[i].Handler) 220 | 221 | req, resp := initCase(cases[i]) 222 | router.ServeHTTP(resp, req) 223 | 224 | assert.Equal(tt, cases[i].ExpStatusCode, resp.Code) 225 | }) 226 | } 227 | } 228 | 229 | func TestWriter_WriteHeaderNow(t *testing.T) { 230 | const ( 231 | testOrigin = "*" 232 | testMethods = "GET,HEAD,POST,PUT,OPTIONS" 233 | ) 234 | 235 | g := gin.New() 236 | g.Use(testNew(time.Second * 3)) 237 | g.Use(func(c *gin.Context) { 238 | if c.Request.Method == http.MethodOptions { 239 | c.Header("Access-Control-Allow-Origin", testOrigin) 240 | c.Header("Access-Control-Allow-Methods", testMethods) 241 | 242 | // Below 3 lines can be replaced with `c.AbortWithStatus(http.StatusNoContent)` 243 | c.Status(http.StatusNoContent) 244 | c.Writer.WriteHeaderNow() 245 | c.Abort() 246 | 247 | return 248 | } 249 | c.Next() 250 | }) 251 | g.GET("/test", func(c *gin.Context) { 252 | c.String(http.StatusOK, "It's works!") 253 | }) 254 | 255 | serv := httptest.NewServer(g) 256 | defer serv.Close() 257 | 258 | req, err := http.NewRequestWithContext( 259 | context.Background(), 260 | http.MethodOptions, 261 | serv.URL+"/test", 262 | nil, 263 | ) 264 | if err != nil { 265 | t.Fatal("NewRequest:", err) 266 | } 267 | resp, err := http.DefaultClient.Do(req) 268 | if err != nil { 269 | t.Fatal("Do request:", err) 270 | } 271 | defer resp.Body.Close() 272 | assert.Equal(t, http.StatusNoContent, resp.StatusCode) 273 | assert.Equal(t, testOrigin, resp.Header.Get("Access-Control-Allow-Origin")) 274 | assert.Equal(t, testMethods, resp.Header.Get("Access-Control-Allow-Methods")) 275 | } 276 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 2 | github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 3 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 4 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 5 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 6 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 12 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 13 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 14 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 15 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 16 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 17 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 18 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 20 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 21 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 22 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 23 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 24 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 25 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 26 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 27 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 28 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 29 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 30 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 34 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 35 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 36 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 37 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 38 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 39 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 40 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 41 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 42 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 43 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 48 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 49 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 50 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 54 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 55 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 56 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 57 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 58 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 59 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 60 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 61 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 62 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 63 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 65 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 66 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 67 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 68 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 69 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 70 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 71 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 72 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 73 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 74 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 75 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 76 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 77 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 78 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 79 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 80 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 82 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 83 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 84 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 85 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 86 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 87 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 88 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 91 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 92 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 93 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 94 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timeout 2 | 3 | [![Run Tests](https://github.com/gin-contrib/timeout/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/gin-contrib/timeout/actions/workflows/go.yml) 4 | [![Trivy Security Scan](https://github.com/gin-contrib/timeout/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/gin-contrib/timeout/actions/workflows/trivy-scan.yml) 5 | [![codecov](https://codecov.io/gh/gin-contrib/timeout/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/timeout) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/timeout)](https://goreportcard.com/report/github.com/gin-contrib/timeout) 7 | [![GoDoc](https://godoc.org/github.com/gin-contrib/timeout?status.svg)](https://pkg.go.dev/github.com/gin-contrib/timeout?tab=doc) 8 | 9 | Timeout is a Gin middleware that wraps a handler and aborts its execution if a specified timeout is reached. This is useful for preventing slow handlers from blocking your server. 10 | 11 | --- 12 | 13 | ## Table of Contents 14 | 15 | - [Timeout](#timeout) 16 | - [Table of Contents](#table-of-contents) 17 | - [Features](#features) 18 | - [Installation](#installation) 19 | - [Quick Start](#quick-start) 20 | - [How It Works](#how-it-works) 21 | - [API Reference](#api-reference) 22 | - [Configuration Options](#configuration-options) 23 | - [`timeout.New(opts ...Option) gin.HandlerFunc`](#timeoutnewopts-option-ginhandlerfunc) 24 | - [Available Options](#available-options) 25 | - [Example](#example) 26 | - [Advanced Usage](#advanced-usage) 27 | - [1. Custom Timeout Response](#1-custom-timeout-response) 28 | - [2. Global Middleware](#2-global-middleware) 29 | - [3. Logging Timeout Events](#3-logging-timeout-events) 30 | - [4. Combining with Other Middleware](#4-combining-with-other-middleware) 31 | - [Real-World Example: Testing Timeout](#real-world-example-testing-timeout) 32 | - [More Examples](#more-examples) 33 | - [Troubleshooting](#troubleshooting) 34 | - [Why is my handler still running after timeout?](#why-is-my-handler-still-running-after-timeout) 35 | - [Why am I getting partial responses?](#why-am-i-getting-partial-responses) 36 | - [What timeout value should I use?](#what-timeout-value-should-i-use) 37 | - [Can I use this with streaming responses?](#can-i-use-this-with-streaming-responses) 38 | - [Does this work with panic recovery?](#does-this-work-with-panic-recovery) 39 | - [Contributing](#contributing) 40 | - [License](#license) 41 | 42 | --- 43 | 44 | ## Features 45 | 46 | - Abort request processing if it exceeds a configurable timeout. 47 | - Customizable timeout response. 48 | - Can be used as route or global middleware. 49 | - Compatible with other Gin middleware. 50 | - Buffered response writer to prevent partial responses. 51 | - Panic recovery within timeout handlers. 52 | 53 | --- 54 | 55 | ## Installation 56 | 57 | ```bash 58 | go get github.com/gin-contrib/timeout 59 | ``` 60 | 61 | --- 62 | 63 | ## Quick Start 64 | 65 | A minimal example that times out a slow handler: 66 | 67 | ```go 68 | // _example/example01/main.go 69 | package main 70 | 71 | import ( 72 | "log" 73 | "net/http" 74 | "time" 75 | 76 | "github.com/gin-contrib/timeout" 77 | "github.com/gin-gonic/gin" 78 | ) 79 | 80 | func emptySuccessResponse(c *gin.Context) { 81 | time.Sleep(200 * time.Microsecond) 82 | c.String(http.StatusOK, "") 83 | } 84 | 85 | func main() { 86 | r := gin.New() 87 | 88 | r.GET("/", timeout.New( 89 | timeout.WithTimeout(100*time.Microsecond), 90 | ), 91 | emptySuccessResponse, 92 | ) 93 | 94 | // Listen and Server in 0.0.0.0:8080 95 | if err := r.Run(":8080"); err != nil { 96 | log.Fatal(err) 97 | } 98 | } 99 | ``` 100 | 101 | In this example, the handler will timeout because it sleeps for 200 microseconds while the timeout is set to 100 microseconds. 102 | 103 | --- 104 | 105 | ## How It Works 106 | 107 | The timeout middleware operates by: 108 | 109 | 1. **Buffering responses**: It wraps the response writer with a buffered writer to prevent partial responses from being sent to the client. 110 | 111 | 2. **Running handlers in goroutines**: Your handler executes in a separate goroutine with a context that can be cancelled. 112 | 113 | 3. **Race against time**: The middleware waits for either: 114 | 115 | - Handler completion (writes buffered response to client) 116 | - Timeout expiry (writes timeout response instead) 117 | - Panic in handler (properly recovers and reports) 118 | 119 | 4. **Important limitation**: Once response headers are written to the client, the timeout response cannot be sent. The middleware can only prevent responses if it catches the timeout before headers are flushed. 120 | 121 | **Default timeout**: If not specified, the default timeout is `5 seconds`. 122 | 123 | --- 124 | 125 | ## API Reference 126 | 127 | ### Configuration Options 128 | 129 | #### `timeout.New(opts ...Option) gin.HandlerFunc` 130 | 131 | Creates a new timeout middleware with the specified options. 132 | 133 | #### Available Options 134 | 135 | | Option | Description | Default | 136 | | --------------------------------------- | -------------------------------------- | -------------------------------------------- | 137 | | `WithTimeout(duration time.Duration)` | Sets the timeout duration | `5 * time.Second` | 138 | | `WithResponse(handler gin.HandlerFunc)` | Sets a custom timeout response handler | Returns HTTP 408 with "Request Timeout" text | 139 | 140 | ### Example 141 | 142 | ```go 143 | timeout.New( 144 | timeout.WithTimeout(3 * time.Second), 145 | timeout.WithResponse(func(c *gin.Context) { 146 | c.JSON(http.StatusRequestTimeout, gin.H{ 147 | "error": "Request took too long", 148 | "code": "TIMEOUT", 149 | }) 150 | }), 151 | ) 152 | ``` 153 | 154 | --- 155 | 156 | ## Advanced Usage 157 | 158 | ### 1. Custom Timeout Response 159 | 160 | You can define a custom response when a timeout occurs: 161 | 162 | ```go 163 | // Custom timeout response for a single route 164 | func testResponse(c *gin.Context) { 165 | c.String(http.StatusRequestTimeout, "custom timeout response") 166 | } 167 | 168 | r.GET("/", timeout.New( 169 | timeout.WithTimeout(100*time.Microsecond), 170 | timeout.WithResponse(testResponse), 171 | ), func(c *gin.Context) { 172 | time.Sleep(200 * time.Microsecond) 173 | c.String(http.StatusOK, "") 174 | }) 175 | ``` 176 | 177 | --- 178 | 179 | ### 2. Global Middleware 180 | 181 | Apply the timeout middleware to all routes: 182 | 183 | ```go 184 | func testResponse(c *gin.Context) { 185 | c.String(http.StatusRequestTimeout, "timeout") 186 | } 187 | 188 | func timeoutMiddleware() gin.HandlerFunc { 189 | return timeout.New( 190 | timeout.WithTimeout(500*time.Millisecond), 191 | timeout.WithResponse(testResponse), 192 | ) 193 | } 194 | 195 | func main() { 196 | r := gin.New() 197 | r.Use(timeoutMiddleware()) 198 | r.GET("/slow", func(c *gin.Context) { 199 | time.Sleep(800 * time.Millisecond) 200 | c.Status(http.StatusOK) 201 | }) 202 | if err := r.Run(":8080"); err != nil { 203 | log.Fatal(err) 204 | } 205 | } 206 | ``` 207 | 208 | --- 209 | 210 | ### 3. Logging Timeout Events 211 | 212 | You can combine the timeout middleware with custom logging for timeout events: 213 | 214 | ```go 215 | import ( 216 | "log/slog" 217 | "net/http" 218 | "time" 219 | 220 | "github.com/gin-contrib/timeout" 221 | "github.com/gin-gonic/gin" 222 | ) 223 | 224 | func main() { 225 | r := gin.Default() 226 | 227 | r.Use(timeout.New( 228 | timeout.WithTimeout(100*time.Microsecond), 229 | ), func(c *gin.Context) { 230 | c.Next() 231 | if c.Writer.Status() == http.StatusRequestTimeout { 232 | slog.Error("request timeout") 233 | } 234 | }) 235 | 236 | r.GET("/long", func(c *gin.Context) { 237 | time.Sleep(10 * time.Second) 238 | c.String(http.StatusOK, "long time ago") 239 | }) 240 | 241 | s := &http.Server{ 242 | Addr: ":8000", 243 | Handler: r, 244 | ReadTimeout: 30 * time.Second, 245 | WriteTimeout: 30 * time.Second, 246 | ReadHeaderTimeout: time.Second * 5, 247 | } 248 | 249 | if err := s.ListenAndServe(); err != nil { 250 | slog.Error("ListenAndServe failed", "err", err) 251 | } 252 | } 253 | ``` 254 | 255 | --- 256 | 257 | ### 4. Combining with Other Middleware 258 | 259 | You can stack the timeout middleware with other middleware, such as authentication or logging: 260 | 261 | ```go 262 | func testResponse(c *gin.Context) { 263 | c.String(http.StatusRequestTimeout, "timeout") 264 | } 265 | 266 | // Custom timeout middleware 267 | func timeoutMiddleware() gin.HandlerFunc { 268 | return timeout.New( 269 | timeout.WithTimeout(500*time.Millisecond), 270 | timeout.WithResponse(testResponse), 271 | ) 272 | } 273 | 274 | // Example auth middleware 275 | func authMiddleware() gin.HandlerFunc { 276 | return func(c *gin.Context) { 277 | debug := c.Query("debug") 278 | if debug != "true" { 279 | c.Next() 280 | return 281 | } 282 | c.AbortWithStatus(401) 283 | } 284 | } 285 | 286 | func main() { 287 | r := gin.New() 288 | r.Use(gin.Logger()) 289 | r.Use(timeoutMiddleware()) 290 | r.Use(authMiddleware()) 291 | r.Use(gin.Recovery()) 292 | 293 | r.GET("/", func(c *gin.Context) { 294 | time.Sleep(1 * time.Second) 295 | c.String(http.StatusOK, "Hello world!") 296 | }) 297 | 298 | if err := r.Run(":8080"); err != nil { 299 | log.Fatal(err) 300 | } 301 | } 302 | ``` 303 | 304 | --- 305 | 306 | ## Real-World Example: Testing Timeout 307 | 308 | Suppose your handler always takes longer than the timeout: 309 | 310 | ```go 311 | // _example/example04/main.go (handler always times out) 312 | r.GET("/", func(c *gin.Context) { 313 | time.Sleep(1 * time.Second) 314 | c.String(http.StatusOK, "Hello world!") 315 | }) 316 | ``` 317 | 318 | With a 500ms timeout, any request will return HTTP 408: 319 | 320 | ```bash 321 | curl -i http://localhost:8080/ 322 | ``` 323 | 324 | **Expected response:** 325 | 326 | ```bash 327 | HTTP/1.1 408 Request Timeout 328 | Content-Type: text/plain; charset=utf-8 329 | 330 | timeout 331 | ``` 332 | 333 | --- 334 | 335 | ## More Examples 336 | 337 | The [`_example`](./_example) directory contains additional usage scenarios: 338 | 339 | | Example | Description | Use Case | 340 | | ----------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------- | 341 | | [example01](./_example/example01/main.go) | Minimal route-level timeout | Quick start - applying timeout to a single route | 342 | | [example02](./_example/example02/main.go) | Global middleware with custom response | Production setup - protecting all endpoints with consistent timeout handling | 343 | | [example03](./_example/example03/main.go) | Logging timeout events + load testing | Monitoring - tracking timeout occurrences with structured logging | 344 | | [example04](./_example/example04/main.go) | Integration with auth middleware | Complex middleware chains - see the [detailed README](./_example/example04/README.md) | 345 | 346 | Explore these examples for practical patterns and advanced integration tips. 347 | 348 | --- 349 | 350 | ## Troubleshooting 351 | 352 | ### Why is my handler still running after timeout? 353 | 354 | **Answer**: The timeout middleware can only prevent the **response** from being sent to the client. It cannot forcefully terminate the goroutine running your handler. However, the client will receive a timeout response and the connection will be closed. 355 | 356 | **Best practice**: Check `c.Request.Context().Done()` in long-running handlers to gracefully exit: 357 | 358 | ```go 359 | r.GET("/long", timeout.New( 360 | timeout.WithTimeout(2*time.Second), 361 | ), func(c *gin.Context) { 362 | for i := 0; i < 10; i++ { 363 | select { 364 | case <-c.Request.Context().Done(): 365 | // Context cancelled, stop processing 366 | return 367 | default: 368 | // Do some work 369 | time.Sleep(500 * time.Millisecond) 370 | } 371 | } 372 | c.String(http.StatusOK, "done") 373 | }) 374 | ``` 375 | 376 | ### Why am I getting partial responses? 377 | 378 | **Answer**: If your handler writes response headers before the timeout occurs, those headers cannot be recalled. The middleware uses a buffered writer to prevent this, but streaming responses or explicit header flushes can bypass the buffer. 379 | 380 | **Solution**: Avoid calling `c.Writer.Flush()` or using streaming responses with timeout middleware. 381 | 382 | ### What timeout value should I use? 383 | 384 | **Guidelines**: 385 | 386 | - **API endpoints**: 5-30 seconds (default is 5s) 387 | - **Database queries**: 3-10 seconds 388 | - **External API calls**: 10-30 seconds 389 | - **Long-running jobs**: Consider using a job queue instead of HTTP with timeout 390 | 391 | **Tip**: Set your timeout slightly lower than your load balancer or reverse proxy timeout to ensure your application responds first. 392 | 393 | ### Can I use this with streaming responses? 394 | 395 | **Not recommended**: The middleware buffers responses to prevent partial writes. Streaming responses (SSE, chunked encoding) are incompatible with this approach. 396 | 397 | **Alternative**: For streaming endpoints, implement timeout logic within your handler using `context.WithTimeout()`. 398 | 399 | ### Does this work with panic recovery? 400 | 401 | **Yes**: The middleware includes panic recovery. In debug mode (`gin.SetMode(gin.DebugMode)`), it will return detailed panic information. In release mode, it re-throws the panic to be handled by upstream middleware like `gin.Recovery()`. 402 | 403 | --- 404 | 405 | ## Contributing 406 | 407 | Contributions are welcome! Please open an issue or submit a pull request. 408 | 409 | ## License 410 | 411 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 412 | --------------------------------------------------------------------------------