├── .github ├── dependabot.yml └── workflows │ ├── bearer.yml │ ├── codeql.yml │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── _example ├── go.mod ├── go.sum └── main.go ├── go.mod ├── go.sum ├── gzip.go ├── gzip_test.go ├── handler.go ├── handler_test.go └── options.go /.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 | -------------------------------------------------------------------------------- /.github/workflows/bearer.yml: -------------------------------------------------------------------------------- 1 | name: Bearer PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | rule_check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Run Report 23 | id: report 24 | uses: bearer/bearer-action@v2 25 | with: 26 | format: rdjson 27 | output: rd.json 28 | diff: true 29 | 30 | - name: Run reviewdog 31 | if: always() 32 | env: 33 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | cat rd.json | reviewdog -f=rdjson -reporter=github-pr-review 36 | -------------------------------------------------------------------------------- /.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@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 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@v3 55 | -------------------------------------------------------------------------------- /.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@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v7 27 | with: 28 | version: v2.0 29 | args: --verbose 30 | test: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | go: [1.23, 1.24] 35 | include: 36 | - os: ubuntu-latest 37 | go-build: ~/.cache/go-build 38 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 39 | runs-on: ${{ matrix.os }} 40 | env: 41 | GO111MODULE: on 42 | GOPROXY: https://proxy.golang.org 43 | steps: 44 | - name: Set up Go ${{ matrix.go }} 45 | uses: actions/setup-go@v5 46 | with: 47 | go-version: ${{ matrix.go }} 48 | 49 | - name: Checkout Code 50 | uses: actions/checkout@v4 51 | with: 52 | ref: ${{ github.ref }} 53 | 54 | - uses: actions/cache@v4 55 | with: 56 | path: | 57 | ${{ matrix.go-build }} 58 | ~/go/pkg/mod 59 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 60 | restore-keys: | 61 | ${{ runner.os }}-go- 62 | - name: Run Tests 63 | run: | 64 | go test -v -covermode=atomic -coverprofile=coverage.out 65 | 66 | - name: Upload coverage to Codecov 67 | uses: codecov/codecov-action@v5 68 | with: 69 | flags: ${{ matrix.os }},go-${{ matrix.go }} 70 | -------------------------------------------------------------------------------- /.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@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - gochecknoinits 11 | - goconst 12 | - gocritic 13 | - gocyclo 14 | - goprintffuncname 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - nolintlint 23 | - rowserrcheck 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - # If true, skip the build. 3 | # Useful for library projects. 4 | # Default is false 5 | skip: true 6 | 7 | changelog: 8 | use: github 9 | groups: 10 | - title: Features 11 | regexp: "^.*feat[(\\w)]*:+.*$" 12 | order: 0 13 | - title: "Bug fixes" 14 | regexp: "^.*fix[(\\w)]*:+.*$" 15 | order: 1 16 | - title: "Enhancements" 17 | regexp: "^.*chore[(\\w)]*:+.*$" 18 | order: 2 19 | - title: "Refactor" 20 | regexp: "^.*refactor[(\\w)]*:+.*$" 21 | order: 3 22 | - title: "Build process updates" 23 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: "Documentation updates" 26 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 27 | order: 4 28 | - title: Others 29 | order: 999 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GZIP gin's middleware 2 | 3 | [![Run Tests](https://github.com/gin-contrib/gzip/actions/workflows/go.yml/badge.svg)](https://github.com/gin-contrib/gzip/actions/workflows/go.yml) 4 | [![codecov](https://codecov.io/gh/gin-contrib/gzip/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/gzip) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gin-contrib/gzip)](https://goreportcard.com/report/github.com/gin-contrib/gzip) 6 | [![GoDoc](https://godoc.org/github.com/gin-contrib/gzip?status.svg)](https://godoc.org/github.com/gin-contrib/gzip) 7 | 8 | Gin middleware to enable `GZIP` support. 9 | 10 | ## Usage 11 | 12 | Download and install it: 13 | 14 | ```sh 15 | go get github.com/gin-contrib/gzip 16 | ``` 17 | 18 | Import it in your code: 19 | 20 | ```go 21 | import "github.com/gin-contrib/gzip" 22 | ``` 23 | 24 | Canonical example: 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "net/http" 32 | "time" 33 | 34 | "github.com/gin-contrib/gzip" 35 | "github.com/gin-gonic/gin" 36 | ) 37 | 38 | func main() { 39 | r := gin.Default() 40 | r.Use(gzip.Gzip(gzip.DefaultCompression)) 41 | r.GET("/ping", func(c *gin.Context) { 42 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 43 | }) 44 | 45 | // Listen and Server in 0.0.0.0:8080 46 | if err := r.Run(":8080"); err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | ``` 51 | 52 | ### Customized Excluded Extensions 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | "net/http" 60 | "time" 61 | 62 | "github.com/gin-contrib/gzip" 63 | "github.com/gin-gonic/gin" 64 | ) 65 | 66 | func main() { 67 | r := gin.Default() 68 | r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp4"}))) 69 | r.GET("/ping", func(c *gin.Context) { 70 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 71 | }) 72 | 73 | // Listen and Server in 0.0.0.0:8080 74 | if err := r.Run(":8080"); err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | ``` 79 | 80 | ### Customized Excluded Paths 81 | 82 | ```go 83 | package main 84 | 85 | import ( 86 | "fmt" 87 | "net/http" 88 | "time" 89 | 90 | "github.com/gin-contrib/gzip" 91 | "github.com/gin-gonic/gin" 92 | ) 93 | 94 | func main() { 95 | r := gin.Default() 96 | r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/api/"}))) 97 | r.GET("/ping", func(c *gin.Context) { 98 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 99 | }) 100 | 101 | // Listen and Server in 0.0.0.0:8080 102 | if err := r.Run(":8080"); err != nil { 103 | log.Fatal(err) 104 | } 105 | } 106 | ``` 107 | 108 | ### Customized Excluded Paths with Regex 109 | 110 | ```go 111 | package main 112 | 113 | import ( 114 | "fmt" 115 | "net/http" 116 | "time" 117 | 118 | "github.com/gin-contrib/gzip" 119 | "github.com/gin-gonic/gin" 120 | ) 121 | 122 | func main() { 123 | r := gin.Default() 124 | r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPathsRegexs([]string{".*"}))) 125 | r.GET("/ping", func(c *gin.Context) { 126 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 127 | }) 128 | 129 | // Listen and Server in 0.0.0.0:8080 130 | if err := r.Run(":8080"); err != nil { 131 | log.Fatal(err) 132 | } 133 | } 134 | ``` 135 | 136 | ### Server Push 137 | 138 | ```go 139 | package main 140 | 141 | import ( 142 | "fmt" 143 | "log" 144 | "net/http" 145 | "time" 146 | 147 | "github.com/gin-contrib/gzip" 148 | "github.com/gin-gonic/gin" 149 | ) 150 | 151 | func main() { 152 | r := gin.Default() 153 | r.Use(gzip.Gzip(gzip.DefaultCompression)) 154 | r.GET("/stream", func(c *gin.Context) { 155 | c.Header("Content-Type", "text/event-stream") 156 | c.Header("Connection", "keep-alive") 157 | for i := 0; i < 10; i++ { 158 | fmt.Fprintf(c.Writer, "id: %d\ndata: tick %d\n\n", i, time.Now().Unix()) 159 | c.Writer.Flush() 160 | time.Sleep(1 * time.Second) 161 | } 162 | }) 163 | 164 | // Listen and Server in 0.0.0.0:8080 165 | if err := r.Run(":8080"); err != nil { 166 | log.Fatal(err) 167 | } 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /_example/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/gin-contrib/gzip v1.1.0 7 | github.com/gin-gonic/gin v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.2 // indirect 12 | github.com/bytedance/sonic/loader v0.2.4 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 15 | github.com/gin-contrib/sse v1.0.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.26.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.2.10 // 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.3 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.2.12 // indirect 29 | golang.org/x/arch v0.15.0 // indirect 30 | golang.org/x/crypto v0.36.0 // indirect 31 | golang.org/x/net v0.38.0 // indirect 32 | golang.org/x/sys v0.31.0 // indirect 33 | golang.org/x/text v0.23.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/gzip => ../ 39 | -------------------------------------------------------------------------------- /_example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 2 | github.com/bytedance/sonic v1.13.2/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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 5 | github.com/bytedance/sonic/loader v0.2.4/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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 13 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 14 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 15 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 16 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 17 | github.com/gin-gonic/gin v1.10.0/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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 25 | github.com/go-playground/validator/v10 v10.26.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.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 35 | github.com/klauspost/cpuid/v2 v2.2.10/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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 47 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 58 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 59 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 60 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 61 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 63 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 64 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 65 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 66 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= 67 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 68 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 69 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 70 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 71 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 72 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 74 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 75 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 76 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 77 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 78 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 79 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 80 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 87 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gin-contrib/gzip" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func main() { 14 | r := gin.Default() 15 | r.Use( 16 | func(c *gin.Context) { 17 | c.Writer.Header().Add("Vary", "Origin") 18 | }, 19 | gzip.Gzip( 20 | gzip.DefaultCompression, 21 | gzip.WithExcludedPaths([]string{"/ping2"}), 22 | )) 23 | 24 | r.GET("/ping", func(c *gin.Context) { 25 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 26 | }) 27 | r.GET("/ping2", func(c *gin.Context) { 28 | c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) 29 | }) 30 | r.GET("/stream", func(c *gin.Context) { 31 | c.Header("Content-Type", "text/event-stream") 32 | c.Header("Connection", "keep-alive") 33 | for i := 0; i < 10; i++ { 34 | fmt.Fprintf(c.Writer, "id: %d\ndata: tick %d\n\n", i, time.Now().Unix()) 35 | c.Writer.Flush() 36 | time.Sleep(1 * time.Second) 37 | } 38 | }) 39 | 40 | // Listen and Server in 0.0.0.0:8080 41 | if err := r.Run(":8080"); err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-contrib/gzip 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.13.2 // indirect 12 | github.com/bytedance/sonic/loader v0.2.4 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // 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.0.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.26.0 // indirect 20 | github.com/goccy/go-json v0.10.5 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.12 // indirect 31 | golang.org/x/arch v0.15.0 // indirect 32 | golang.org/x/crypto v0.36.0 // indirect 33 | golang.org/x/net v0.38.0 // indirect 34 | golang.org/x/sys v0.31.0 // indirect 35 | golang.org/x/text v0.23.0 // indirect 36 | google.golang.org/protobuf v1.36.6 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 2 | github.com/bytedance/sonic v1.13.2/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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 5 | github.com/bytedance/sonic/loader v0.2.4/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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 13 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 14 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 15 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 16 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 17 | github.com/gin-gonic/gin v1.10.0/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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 25 | github.com/go-playground/validator/v10 v10.26.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.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 35 | github.com/klauspost/cpuid/v2 v2.2.10/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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 47 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 58 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 59 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 60 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 61 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 63 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 64 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 65 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 66 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= 67 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 68 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 69 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 70 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 71 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 72 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 74 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 75 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 76 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 77 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 78 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 79 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 80 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 87 | -------------------------------------------------------------------------------- /gzip.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "errors" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | const ( 14 | BestCompression = gzip.BestCompression 15 | BestSpeed = gzip.BestSpeed 16 | DefaultCompression = gzip.DefaultCompression 17 | NoCompression = gzip.NoCompression 18 | HuffmanOnly = gzip.HuffmanOnly 19 | ) 20 | 21 | func Gzip(level int, options ...Option) gin.HandlerFunc { 22 | return newGzipHandler(level, options...).Handle 23 | } 24 | 25 | type gzipWriter struct { 26 | gin.ResponseWriter 27 | writer *gzip.Writer 28 | } 29 | 30 | func (g *gzipWriter) WriteString(s string) (int, error) { 31 | g.Header().Del("Content-Length") 32 | return g.writer.Write([]byte(s)) 33 | } 34 | 35 | func (g *gzipWriter) Write(data []byte) (int, error) { 36 | g.Header().Del("Content-Length") 37 | return g.writer.Write(data) 38 | } 39 | 40 | func (g *gzipWriter) Flush() { 41 | _ = g.writer.Flush() 42 | g.ResponseWriter.Flush() 43 | } 44 | 45 | // Fix: https://github.com/mholt/caddy/issues/38 46 | func (g *gzipWriter) WriteHeader(code int) { 47 | g.Header().Del("Content-Length") 48 | g.ResponseWriter.WriteHeader(code) 49 | } 50 | 51 | // Ensure gzipWriter implements the http.Hijacker interface. 52 | // This will cause a compile-time error if gzipWriter does not implement all methods of the http.Hijacker interface. 53 | var _ http.Hijacker = (*gzipWriter)(nil) 54 | 55 | // Hijack allows the caller to take over the connection from the HTTP server. 56 | // After a call to Hijack, the HTTP server library will not do anything else with the connection. 57 | // It becomes the caller's responsibility to manage and close the connection. 58 | // 59 | // It returns the underlying net.Conn, a buffered reader/writer for the connection, and an error 60 | // if the ResponseWriter does not support the Hijacker interface. 61 | func (g *gzipWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 62 | hijacker, ok := g.ResponseWriter.(http.Hijacker) 63 | if !ok { 64 | return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") 65 | } 66 | return hijacker.Hijack() 67 | } 68 | -------------------------------------------------------------------------------- /gzip_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "context" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/http/httptest" 13 | "net/http/httputil" 14 | "net/url" 15 | "strconv" 16 | "testing" 17 | 18 | "github.com/gin-gonic/gin" 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | const ( 24 | testResponse = "Gzip Test Response " 25 | testReverseResponse = "Gzip Test Reverse Response " 26 | ) 27 | 28 | type rServer struct{} 29 | 30 | func (s *rServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 31 | fmt.Fprint(rw, testReverseResponse) 32 | } 33 | 34 | type closeNotifyingRecorder struct { 35 | *httptest.ResponseRecorder 36 | closed chan bool 37 | } 38 | 39 | func newCloseNotifyingRecorder() *closeNotifyingRecorder { 40 | return &closeNotifyingRecorder{ 41 | httptest.NewRecorder(), 42 | make(chan bool, 1), 43 | } 44 | } 45 | 46 | func (c *closeNotifyingRecorder) CloseNotify() <-chan bool { 47 | return c.closed 48 | } 49 | 50 | func newServer() *gin.Engine { 51 | // init reverse proxy server 52 | rServer := httptest.NewServer(new(rServer)) 53 | target, _ := url.Parse(rServer.URL) 54 | rp := httputil.NewSingleHostReverseProxy(target) 55 | 56 | router := gin.New() 57 | router.Use(Gzip(DefaultCompression)) 58 | router.GET("/", func(c *gin.Context) { 59 | c.Header("Content-Length", strconv.Itoa(len(testResponse))) 60 | c.String(200, testResponse) 61 | }) 62 | router.GET("/ping", func(c *gin.Context) { 63 | c.Writer.Header().Add("Vary", "Origin") 64 | }, func(c *gin.Context) { 65 | c.Header("Content-Length", strconv.Itoa(len(testResponse))) 66 | c.String(200, testResponse) 67 | }) 68 | router.Any("/reverse", func(c *gin.Context) { 69 | rp.ServeHTTP(c.Writer, c.Request) 70 | }) 71 | return router 72 | } 73 | 74 | func TestVaryHeader(t *testing.T) { 75 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/ping", nil) 76 | req.Header.Add(headerAcceptEncoding, "gzip") 77 | 78 | w := httptest.NewRecorder() 79 | r := newServer() 80 | r.ServeHTTP(w, req) 81 | 82 | assert.Equal(t, 200, w.Code) 83 | assert.Equal(t, "gzip", w.Header().Get(headerContentEncoding)) 84 | assert.Equal(t, []string{headerAcceptEncoding, "Origin"}, w.Header().Values(headerVary)) 85 | assert.NotEqual(t, "0", w.Header().Get("Content-Length")) 86 | assert.NotEqual(t, 19, w.Body.Len()) 87 | assert.Equal(t, w.Header().Get("Content-Length"), fmt.Sprint(w.Body.Len())) 88 | 89 | gr, err := gzip.NewReader(w.Body) 90 | assert.NoError(t, err) 91 | defer gr.Close() 92 | 93 | body, _ := io.ReadAll(gr) 94 | assert.Equal(t, testResponse, string(body)) 95 | } 96 | 97 | func TestGzip(t *testing.T) { 98 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 99 | req.Header.Add(headerAcceptEncoding, "gzip") 100 | 101 | w := httptest.NewRecorder() 102 | r := newServer() 103 | r.ServeHTTP(w, req) 104 | 105 | assert.Equal(t, w.Code, 200) 106 | assert.Equal(t, w.Header().Get(headerContentEncoding), "gzip") 107 | assert.Equal(t, w.Header().Get(headerVary), headerAcceptEncoding) 108 | assert.NotEqual(t, w.Header().Get("Content-Length"), "0") 109 | assert.NotEqual(t, w.Body.Len(), 19) 110 | assert.Equal(t, fmt.Sprint(w.Body.Len()), w.Header().Get("Content-Length")) 111 | 112 | gr, err := gzip.NewReader(w.Body) 113 | assert.NoError(t, err) 114 | defer gr.Close() 115 | 116 | body, _ := io.ReadAll(gr) 117 | assert.Equal(t, string(body), testResponse) 118 | } 119 | 120 | func TestGzipPNG(t *testing.T) { 121 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/image.png", nil) 122 | req.Header.Add(headerAcceptEncoding, "gzip") 123 | 124 | router := gin.New() 125 | router.Use(Gzip(DefaultCompression)) 126 | router.GET("/image.png", func(c *gin.Context) { 127 | c.String(200, "this is a PNG!") 128 | }) 129 | 130 | w := httptest.NewRecorder() 131 | router.ServeHTTP(w, req) 132 | 133 | assert.Equal(t, w.Code, 200) 134 | assert.Equal(t, w.Header().Get(headerContentEncoding), "") 135 | assert.Equal(t, w.Header().Get(headerVary), "") 136 | assert.Equal(t, w.Body.String(), "this is a PNG!") 137 | } 138 | 139 | func TestExcludedPathsAndExtensions(t *testing.T) { 140 | tests := []struct { 141 | path string 142 | option Option 143 | expectedContentEncoding string 144 | expectedVary string 145 | expectedBody string 146 | expectedContentLength string 147 | }{ 148 | {"/api/books", WithExcludedPaths([]string{"/api/"}), "", "", "this is books!", ""}, 149 | {"/index.html", WithExcludedExtensions([]string{".html"}), "", "", "this is a HTML!", ""}, 150 | } 151 | 152 | for _, tt := range tests { 153 | req, _ := http.NewRequestWithContext(context.Background(), "GET", tt.path, nil) 154 | req.Header.Add(headerAcceptEncoding, "gzip") 155 | 156 | router := gin.New() 157 | router.Use(Gzip(DefaultCompression, tt.option)) 158 | router.GET(tt.path, func(c *gin.Context) { 159 | c.String(200, tt.expectedBody) 160 | }) 161 | 162 | w := httptest.NewRecorder() 163 | router.ServeHTTP(w, req) 164 | 165 | assert.Equal(t, http.StatusOK, w.Code) 166 | assert.Equal(t, tt.expectedContentEncoding, w.Header().Get(headerContentEncoding)) 167 | assert.Equal(t, tt.expectedVary, w.Header().Get(headerVary)) 168 | assert.Equal(t, tt.expectedBody, w.Body.String()) 169 | assert.Equal(t, tt.expectedContentLength, w.Header().Get("Content-Length")) 170 | } 171 | } 172 | 173 | func TestNoGzip(t *testing.T) { 174 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 175 | 176 | w := httptest.NewRecorder() 177 | r := newServer() 178 | r.ServeHTTP(w, req) 179 | 180 | assert.Equal(t, w.Code, 200) 181 | assert.Equal(t, w.Header().Get(headerContentEncoding), "") 182 | assert.Equal(t, w.Header().Get("Content-Length"), "19") 183 | assert.Equal(t, w.Body.String(), testResponse) 184 | } 185 | 186 | func TestGzipWithReverseProxy(t *testing.T) { 187 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/reverse", nil) 188 | req.Header.Add(headerAcceptEncoding, "gzip") 189 | 190 | w := newCloseNotifyingRecorder() 191 | r := newServer() 192 | r.ServeHTTP(w, req) 193 | 194 | assert.Equal(t, w.Code, 200) 195 | assert.Equal(t, w.Header().Get(headerContentEncoding), "gzip") 196 | assert.Equal(t, w.Header().Get(headerVary), headerAcceptEncoding) 197 | assert.NotEqual(t, w.Header().Get("Content-Length"), "0") 198 | assert.NotEqual(t, w.Body.Len(), 19) 199 | assert.Equal(t, fmt.Sprint(w.Body.Len()), w.Header().Get("Content-Length")) 200 | 201 | gr, err := gzip.NewReader(w.Body) 202 | assert.NoError(t, err) 203 | defer gr.Close() 204 | 205 | body, _ := io.ReadAll(gr) 206 | assert.Equal(t, string(body), testReverseResponse) 207 | } 208 | 209 | func TestDecompressGzip(t *testing.T) { 210 | buf := &bytes.Buffer{} 211 | gz, _ := gzip.NewWriterLevel(buf, gzip.DefaultCompression) 212 | if _, err := gz.Write([]byte(testResponse)); err != nil { 213 | gz.Close() 214 | t.Fatal(err) 215 | } 216 | gz.Close() 217 | 218 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf) 219 | req.Header.Add(headerContentEncoding, "gzip") 220 | 221 | router := gin.New() 222 | router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle))) 223 | router.POST("/", func(c *gin.Context) { 224 | if v := c.Request.Header.Get(headerContentEncoding); v != "" { 225 | t.Errorf("unexpected `Content-Encoding`: %s header", v) 226 | } 227 | if v := c.Request.Header.Get("Content-Length"); v != "" { 228 | t.Errorf("unexpected `Content-Length`: %s header", v) 229 | } 230 | data, err := c.GetRawData() 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | c.Data(200, "text/plain", data) 235 | }) 236 | 237 | w := httptest.NewRecorder() 238 | router.ServeHTTP(w, req) 239 | 240 | assert.Equal(t, http.StatusOK, w.Code) 241 | assert.Equal(t, "", w.Header().Get(headerContentEncoding)) 242 | assert.Equal(t, "", w.Header().Get(headerVary)) 243 | assert.Equal(t, testResponse, w.Body.String()) 244 | assert.Equal(t, "", w.Header().Get("Content-Length")) 245 | } 246 | 247 | func TestDecompressGzipWithEmptyBody(t *testing.T) { 248 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", nil) 249 | req.Header.Add(headerContentEncoding, "gzip") 250 | 251 | router := gin.New() 252 | router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle))) 253 | router.POST("/", func(c *gin.Context) { 254 | c.String(200, "ok") 255 | }) 256 | 257 | w := httptest.NewRecorder() 258 | router.ServeHTTP(w, req) 259 | 260 | assert.Equal(t, http.StatusOK, w.Code) 261 | assert.Equal(t, "", w.Header().Get(headerContentEncoding)) 262 | assert.Equal(t, "", w.Header().Get(headerVary)) 263 | assert.Equal(t, "ok", w.Body.String()) 264 | assert.Equal(t, "", w.Header().Get("Content-Length")) 265 | } 266 | 267 | func TestDecompressGzipWithIncorrectData(t *testing.T) { 268 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", bytes.NewReader([]byte(testResponse))) 269 | req.Header.Add(headerContentEncoding, "gzip") 270 | 271 | router := gin.New() 272 | router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle))) 273 | router.POST("/", func(c *gin.Context) { 274 | c.String(200, "ok") 275 | }) 276 | 277 | w := httptest.NewRecorder() 278 | router.ServeHTTP(w, req) 279 | 280 | assert.Equal(t, http.StatusBadRequest, w.Code) 281 | } 282 | 283 | func TestDecompressOnly(t *testing.T) { 284 | buf := &bytes.Buffer{} 285 | gz, _ := gzip.NewWriterLevel(buf, gzip.DefaultCompression) 286 | if _, err := gz.Write([]byte(testResponse)); err != nil { 287 | gz.Close() 288 | t.Fatal(err) 289 | } 290 | gz.Close() 291 | 292 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf) 293 | req.Header.Add(headerContentEncoding, "gzip") 294 | 295 | router := gin.New() 296 | router.Use(Gzip(NoCompression, WithDecompressOnly(), WithDecompressFn(DefaultDecompressHandle))) 297 | router.POST("/", func(c *gin.Context) { 298 | if v := c.Request.Header.Get(headerContentEncoding); v != "" { 299 | t.Errorf("unexpected `Content-Encoding`: %s header", v) 300 | } 301 | if v := c.Request.Header.Get("Content-Length"); v != "" { 302 | t.Errorf("unexpected `Content-Length`: %s header", v) 303 | } 304 | data, err := c.GetRawData() 305 | if err != nil { 306 | t.Fatal(err) 307 | } 308 | c.Data(200, "text/plain", data) 309 | }) 310 | 311 | w := httptest.NewRecorder() 312 | router.ServeHTTP(w, req) 313 | 314 | assert.Equal(t, http.StatusOK, w.Code) 315 | assert.Equal(t, "", w.Header().Get(headerContentEncoding)) 316 | assert.Equal(t, "", w.Header().Get(headerVary)) 317 | assert.Equal(t, testResponse, w.Body.String()) 318 | assert.Equal(t, "", w.Header().Get("Content-Length")) 319 | } 320 | 321 | func TestGzipWithDecompressOnly(t *testing.T) { 322 | buf := &bytes.Buffer{} 323 | gz, _ := gzip.NewWriterLevel(buf, gzip.DefaultCompression) 324 | if _, err := gz.Write([]byte(testResponse)); err != nil { 325 | gz.Close() 326 | t.Fatal(err) 327 | } 328 | gz.Close() 329 | 330 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf) 331 | req.Header.Add(headerContentEncoding, "gzip") 332 | req.Header.Add(headerAcceptEncoding, "gzip") 333 | 334 | r := gin.New() 335 | r.Use(Gzip(NoCompression, WithDecompressOnly(), WithDecompressFn(DefaultDecompressHandle))) 336 | r.POST("/", func(c *gin.Context) { 337 | assert.Equal(t, c.Request.Header.Get(headerContentEncoding), "") 338 | assert.Equal(t, c.Request.Header.Get("Content-Length"), "") 339 | body, err := c.GetRawData() 340 | if err != nil { 341 | t.Fatal(err) 342 | } 343 | assert.Equal(t, testResponse, string(body)) 344 | c.String(200, testResponse) 345 | }) 346 | 347 | w := httptest.NewRecorder() 348 | r.ServeHTTP(w, req) 349 | 350 | assert.Equal(t, 200, w.Code) 351 | assert.Equal(t, "", w.Header().Get(headerContentEncoding)) 352 | assert.Equal(t, testResponse, w.Body.String()) 353 | } 354 | 355 | func TestCustomShouldCompressFn(t *testing.T) { 356 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 357 | req.Header.Add(headerAcceptEncoding, "gzip") 358 | 359 | router := gin.New() 360 | router.Use(Gzip( 361 | DefaultCompression, 362 | WithCustomShouldCompressFn(func(_ *gin.Context) bool { 363 | return false 364 | }), 365 | )) 366 | router.GET("/", func(c *gin.Context) { 367 | c.Header("Content-Length", strconv.Itoa(len(testResponse))) 368 | c.String(200, testResponse) 369 | }) 370 | 371 | w := httptest.NewRecorder() 372 | router.ServeHTTP(w, req) 373 | 374 | assert.Equal(t, 200, w.Code) 375 | assert.Equal(t, "", w.Header().Get(headerContentEncoding)) 376 | assert.Equal(t, "19", w.Header().Get("Content-Length")) 377 | assert.Equal(t, testResponse, w.Body.String()) 378 | } 379 | 380 | type hijackableResponse struct { 381 | Hijacked bool 382 | header http.Header 383 | } 384 | 385 | func newHijackableResponse() *hijackableResponse { 386 | return &hijackableResponse{header: make(http.Header)} 387 | } 388 | 389 | func (h *hijackableResponse) Header() http.Header { return h.header } 390 | func (h *hijackableResponse) Write([]byte) (int, error) { return 0, nil } 391 | func (h *hijackableResponse) WriteHeader(int) {} 392 | func (h *hijackableResponse) Flush() {} 393 | func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) { 394 | h.Hijacked = true 395 | return nil, nil, nil 396 | } 397 | 398 | func TestResponseWriterHijack(t *testing.T) { 399 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 400 | req.Header.Add(headerAcceptEncoding, "gzip") 401 | 402 | router := gin.New() 403 | router.Use(Gzip( 404 | DefaultCompression, 405 | WithCustomShouldCompressFn(func(_ *gin.Context) bool { 406 | return false 407 | }), 408 | )).Use(gin.HandlerFunc(func(c *gin.Context) { 409 | hj, ok := c.Writer.(http.Hijacker) 410 | require.True(t, ok) 411 | 412 | _, _, err := hj.Hijack() 413 | assert.Nil(t, err) 414 | c.Next() 415 | })) 416 | router.GET("/", func(c *gin.Context) { 417 | c.Header("Content-Length", strconv.Itoa(len(testResponse))) 418 | c.String(200, testResponse) 419 | }) 420 | 421 | hijackable := newHijackableResponse() 422 | router.ServeHTTP(hijackable, req) 423 | assert.True(t, hijackable.Hijacked) 424 | } 425 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "net/http" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | const ( 16 | headerAcceptEncoding = "Accept-Encoding" 17 | headerContentEncoding = "Content-Encoding" 18 | headerVary = "Vary" 19 | ) 20 | 21 | type gzipHandler struct { 22 | *config 23 | gzPool sync.Pool 24 | } 25 | 26 | func isCompressionLevelValid(level int) bool { 27 | return level == gzip.DefaultCompression || 28 | level == gzip.NoCompression || 29 | (level >= gzip.BestSpeed && level <= gzip.BestCompression) 30 | } 31 | 32 | func newGzipHandler(level int, opts ...Option) *gzipHandler { 33 | cfg := &config{ 34 | excludedExtensions: DefaultExcludedExtentions, 35 | } 36 | 37 | // Apply each option to the config 38 | for _, o := range opts { 39 | o.apply(cfg) 40 | } 41 | 42 | if !isCompressionLevelValid(level) { 43 | // For web content, level 4 seems to be a sweet spot. 44 | level = 4 45 | } 46 | 47 | handler := &gzipHandler{ 48 | config: cfg, 49 | gzPool: sync.Pool{ 50 | New: func() interface{} { 51 | gz, _ := gzip.NewWriterLevel(io.Discard, level) 52 | return gz 53 | }, 54 | }, 55 | } 56 | return handler 57 | } 58 | 59 | // Handle is a middleware function for handling gzip compression in HTTP requests and responses. 60 | // It first checks if the request has a "Content-Encoding" header set to "gzip" and if a decompression 61 | // function is provided, it will call the decompression function. If the handler is set to decompress only, 62 | // or if the custom compression decision function indicates not to compress, it will return early. 63 | // Otherwise, it retrieves a gzip.Writer from the pool, sets the necessary response headers for gzip encoding, 64 | // and wraps the response writer with a gzipWriter. After the request is processed, it ensures the gzip.Writer 65 | // is properly closed and the "Content-Length" header is set based on the response size. 66 | func (g *gzipHandler) Handle(c *gin.Context) { 67 | if fn := g.decompressFn; fn != nil && strings.Contains(c.Request.Header.Get("Content-Encoding"), "gzip") { 68 | fn(c) 69 | } 70 | 71 | if g.decompressOnly || 72 | (g.customShouldCompressFn != nil && !g.customShouldCompressFn(c)) || 73 | (g.customShouldCompressFn == nil && !g.shouldCompress(c.Request)) { 74 | return 75 | } 76 | 77 | gz := g.gzPool.Get().(*gzip.Writer) 78 | gz.Reset(c.Writer) 79 | 80 | c.Header(headerContentEncoding, "gzip") 81 | c.Writer.Header().Add(headerVary, headerAcceptEncoding) 82 | // check ETag Header 83 | originalEtag := c.GetHeader("ETag") 84 | if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") { 85 | c.Header("ETag", "W/"+originalEtag) 86 | } 87 | c.Writer = &gzipWriter{c.Writer, gz} 88 | defer func() { 89 | if c.Writer.Size() < 0 { 90 | // do not write gzip footer when nothing is written to the response body 91 | gz.Reset(io.Discard) 92 | } 93 | _ = gz.Close() 94 | if c.Writer.Size() > -1 { 95 | c.Header("Content-Length", strconv.Itoa(c.Writer.Size())) 96 | } 97 | g.gzPool.Put(gz) 98 | }() 99 | c.Next() 100 | } 101 | 102 | func (g *gzipHandler) shouldCompress(req *http.Request) bool { 103 | if !strings.Contains(req.Header.Get(headerAcceptEncoding), "gzip") || 104 | strings.Contains(req.Header.Get("Connection"), "Upgrade") { 105 | return false 106 | } 107 | 108 | // Check if the request path is excluded from compression 109 | extension := filepath.Ext(req.URL.Path) 110 | if g.excludedExtensions.Contains(extension) || 111 | g.excludedPaths.Contains(req.URL.Path) || 112 | g.excludedPathesRegexs.Contains(req.URL.Path) { 113 | return false 114 | } 115 | 116 | return true 117 | } 118 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestHandleGzip(t *testing.T) { 17 | gin.SetMode(gin.TestMode) 18 | 19 | tests := []struct { 20 | name string 21 | path string 22 | acceptEncoding string 23 | expectedContentEncoding string 24 | expectedBody string 25 | }{ 26 | { 27 | name: "Gzip compression", 28 | path: "/", 29 | acceptEncoding: "gzip", 30 | expectedContentEncoding: "gzip", 31 | expectedBody: "Gzip Test Response", 32 | }, 33 | { 34 | name: "No compression", 35 | path: "/", 36 | acceptEncoding: "", 37 | expectedContentEncoding: "", 38 | expectedBody: "Gzip Test Response", 39 | }, 40 | } 41 | 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | router := gin.New() 45 | router.Use(Gzip(DefaultCompression)) 46 | router.GET("/", func(c *gin.Context) { 47 | c.String(http.StatusOK, "Gzip Test Response") 48 | }) 49 | 50 | req, _ := http.NewRequestWithContext(context.Background(), "GET", tt.path, nil) 51 | req.Header.Set(headerAcceptEncoding, tt.acceptEncoding) 52 | 53 | w := httptest.NewRecorder() 54 | router.ServeHTTP(w, req) 55 | 56 | assert.Equal(t, http.StatusOK, w.Code) 57 | assert.Equal(t, tt.expectedContentEncoding, w.Header().Get("Content-Encoding")) 58 | 59 | if tt.expectedContentEncoding == "gzip" { 60 | gr, err := gzip.NewReader(w.Body) 61 | assert.NoError(t, err) 62 | defer gr.Close() 63 | 64 | body, _ := io.ReadAll(gr) 65 | assert.Equal(t, tt.expectedBody, string(body)) 66 | } else { 67 | assert.Equal(t, tt.expectedBody, w.Body.String()) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestHandleDecompressGzip(t *testing.T) { 74 | gin.SetMode(gin.TestMode) 75 | 76 | buf := &bytes.Buffer{} 77 | gz, _ := gzip.NewWriterLevel(buf, gzip.DefaultCompression) 78 | if _, err := gz.Write([]byte("Gzip Test Response")); err != nil { 79 | gz.Close() 80 | t.Fatal(err) 81 | } 82 | gz.Close() 83 | 84 | router := gin.New() 85 | router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle))) 86 | router.POST("/", func(c *gin.Context) { 87 | data, err := c.GetRawData() 88 | assert.NoError(t, err) 89 | assert.Equal(t, "Gzip Test Response", string(data)) 90 | c.String(http.StatusOK, "ok") 91 | }) 92 | 93 | req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf) 94 | req.Header.Set("Content-Encoding", "gzip") 95 | 96 | w := httptest.NewRecorder() 97 | router.ServeHTTP(w, req) 98 | 99 | assert.Equal(t, http.StatusOK, w.Code) 100 | assert.Equal(t, "ok", w.Body.String()) 101 | } 102 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | var ( 15 | // DefaultExcludedExtentions is a predefined list of file extensions that should be excluded from gzip compression. 16 | // These extensions typically represent image files that are already compressed 17 | // and do not benefit from additional compression. 18 | DefaultExcludedExtentions = NewExcludedExtensions([]string{ 19 | ".png", ".gif", ".jpeg", ".jpg", 20 | }) 21 | // ErrUnsupportedContentEncoding is an error that indicates the content encoding 22 | // is not supported by the application. 23 | ErrUnsupportedContentEncoding = errors.New("unsupported content encoding") 24 | ) 25 | 26 | // Option is an interface that defines a method to apply a configuration 27 | // to a given config instance. Implementations of this interface can be 28 | // used to modify the configuration settings of the logger. 29 | type Option interface { 30 | apply(*config) 31 | } 32 | 33 | // Ensures that optionFunc implements the Option interface at compile time. 34 | // If optionFunc does not implement Option, a compile-time error will occur. 35 | var _ Option = (*optionFunc)(nil) 36 | 37 | type optionFunc func(*config) 38 | 39 | func (o optionFunc) apply(c *config) { 40 | o(c) 41 | } 42 | 43 | type config struct { 44 | excludedExtensions ExcludedExtensions 45 | excludedPaths ExcludedPaths 46 | excludedPathesRegexs ExcludedPathesRegexs 47 | decompressFn func(c *gin.Context) 48 | decompressOnly bool 49 | customShouldCompressFn func(c *gin.Context) bool 50 | } 51 | 52 | // WithExcludedExtensions returns an Option that sets the ExcludedExtensions field of the Options struct. 53 | // Parameters: 54 | // - args: []string - A slice of file extensions to exclude from gzip compression. 55 | func WithExcludedExtensions(args []string) Option { 56 | return optionFunc(func(o *config) { 57 | o.excludedExtensions = NewExcludedExtensions(args) 58 | }) 59 | } 60 | 61 | // WithExcludedPaths returns an Option that sets the ExcludedPaths field of the Options struct. 62 | // Parameters: 63 | // - args: []string - A slice of paths to exclude from gzip compression. 64 | func WithExcludedPaths(args []string) Option { 65 | return optionFunc(func(o *config) { 66 | o.excludedPaths = NewExcludedPaths(args) 67 | }) 68 | } 69 | 70 | // WithExcludedPathsRegexs returns an Option that sets the ExcludedPathesRegexs field of the Options struct. 71 | // Parameters: 72 | // - args: []string - A slice of regex patterns to exclude paths from gzip compression. 73 | func WithExcludedPathsRegexs(args []string) Option { 74 | return optionFunc(func(o *config) { 75 | o.excludedPathesRegexs = NewExcludedPathesRegexs(args) 76 | }) 77 | } 78 | 79 | // WithDecompressFn returns an Option that sets the DecompressFn field of the Options struct. 80 | // Parameters: 81 | // - decompressFn: func(c *gin.Context) - A function to handle decompression of incoming requests. 82 | func WithDecompressFn(decompressFn func(c *gin.Context)) Option { 83 | return optionFunc(func(o *config) { 84 | o.decompressFn = decompressFn 85 | }) 86 | } 87 | 88 | // WithDecompressOnly is an option that configures the gzip middleware to only 89 | // decompress incoming requests without compressing the responses. When this 90 | // option is enabled, the middleware will set the DecompressOnly field of the 91 | // Options struct to true. 92 | func WithDecompressOnly() Option { 93 | return optionFunc(func(o *config) { 94 | o.decompressOnly = true 95 | }) 96 | } 97 | 98 | // WithCustomShouldCompressFn returns an Option that sets the CustomShouldCompressFn field of the Options struct. 99 | // Parameters: 100 | // - fn: func(c *gin.Context) bool - A function to determine if a request should be compressed. 101 | // The function should return true if the request should be compressed, false otherwise. 102 | // If the function returns false, the middleware will not compress the response. 103 | // If the function is nil, the middleware will use the default logic to determine 104 | // if the response should be compressed. 105 | // 106 | // Returns: 107 | // - Option - An option that sets the CustomShouldCompressFn field of the Options struct. 108 | // 109 | // Example: 110 | // 111 | // router.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithCustomShouldCompressFn(func(c *gin.Context) bool { 112 | // return c.Request.URL.Path != "/no-compress" 113 | // }))) 114 | func WithCustomShouldCompressFn(fn func(c *gin.Context) bool) Option { 115 | return optionFunc(func(o *config) { 116 | o.customShouldCompressFn = fn 117 | }) 118 | } 119 | 120 | // Using map for better lookup performance 121 | type ExcludedExtensions map[string]struct{} 122 | 123 | // NewExcludedExtensions creates a new ExcludedExtensions map from a slice of file extensions. 124 | // Parameters: 125 | // - extensions: []string - A slice of file extensions to exclude from gzip compression. 126 | // 127 | // Returns: 128 | // - ExcludedExtensions - A map of excluded file extensions. 129 | func NewExcludedExtensions(extensions []string) ExcludedExtensions { 130 | res := make(ExcludedExtensions, len(extensions)) 131 | for _, e := range extensions { 132 | res[e] = struct{}{} 133 | } 134 | return res 135 | } 136 | 137 | // Contains checks if a given file extension is in the ExcludedExtensions map. 138 | // Parameters: 139 | // - target: string - The file extension to check. 140 | // 141 | // Returns: 142 | // - bool - True if the extension is excluded, false otherwise. 143 | func (e ExcludedExtensions) Contains(target string) bool { 144 | _, ok := e[target] 145 | return ok 146 | } 147 | 148 | type ExcludedPaths []string 149 | 150 | // NewExcludedPaths creates a new ExcludedPaths slice from a slice of paths. 151 | // Parameters: 152 | // - paths: []string - A slice of paths to exclude from gzip compression. 153 | // 154 | // Returns: 155 | // - ExcludedPaths - A slice of excluded paths. 156 | func NewExcludedPaths(paths []string) ExcludedPaths { 157 | return ExcludedPaths(paths) 158 | } 159 | 160 | // Contains checks if a given request URI starts with any of the excluded paths. 161 | // Parameters: 162 | // - requestURI: string - The request URI to check. 163 | // 164 | // Returns: 165 | // - bool - True if the URI starts with an excluded path, false otherwise. 166 | func (e ExcludedPaths) Contains(requestURI string) bool { 167 | for _, path := range e { 168 | if strings.HasPrefix(requestURI, path) { 169 | return true 170 | } 171 | } 172 | return false 173 | } 174 | 175 | type ExcludedPathesRegexs []*regexp.Regexp 176 | 177 | // NewExcludedPathesRegexs creates a new ExcludedPathesRegexs slice from a slice of regex patterns. 178 | // Parameters: 179 | // - regexs: []string - A slice of regex patterns to exclude paths from gzip compression. 180 | // 181 | // Returns: 182 | // - ExcludedPathesRegexs - A slice of excluded path regex patterns. 183 | func NewExcludedPathesRegexs(regexs []string) ExcludedPathesRegexs { 184 | result := make(ExcludedPathesRegexs, len(regexs)) 185 | for i, reg := range regexs { 186 | result[i] = regexp.MustCompile(reg) 187 | } 188 | return result 189 | } 190 | 191 | // Contains checks if a given request URI matches any of the excluded path regex patterns. 192 | // Parameters: 193 | // - requestURI: string - The request URI to check. 194 | // 195 | // Returns: 196 | // - bool - True if the URI matches an excluded path regex pattern, false otherwise. 197 | func (e ExcludedPathesRegexs) Contains(requestURI string) bool { 198 | for _, reg := range e { 199 | if reg.MatchString(requestURI) { 200 | return true 201 | } 202 | } 203 | return false 204 | } 205 | 206 | // DefaultDecompressHandle is a middleware function for the Gin framework that 207 | // decompresses the request body if it is gzip encoded. It checks if the request 208 | // body is nil and returns immediately if it is. Otherwise, it attempts to create 209 | // a new gzip reader from the request body. If an error occurs during this process, 210 | // it aborts the request with a 400 Bad Request status and the error. If successful, 211 | // it removes the "Content-Encoding" and "Content-Length" headers from the request 212 | // and replaces the request body with the decompressed reader. 213 | // 214 | // Parameters: 215 | // - c: *gin.Context - The Gin context for the current request. 216 | func DefaultDecompressHandle(c *gin.Context) { 217 | if c.Request.Body == nil { 218 | return 219 | } 220 | 221 | contentEncodingField := strings.Split(strings.ToLower(c.GetHeader("Content-Encoding")), ",") 222 | if len(contentEncodingField) == 0 { // nothing to decompress 223 | c.Next() 224 | 225 | return 226 | } 227 | 228 | toClose := make([]io.Closer, 0, len(contentEncodingField)) 229 | defer func() { 230 | for i := len(toClose); i > 0; i-- { 231 | toClose[i-1].Close() 232 | } 233 | }() 234 | 235 | // parses multiply gzips like 236 | // Content-Encoding: gzip, gzip, gzip 237 | // allowed by RFC 238 | for i := 0; i < len(contentEncodingField); i++ { 239 | trimmedValue := strings.TrimSpace(contentEncodingField[i]) 240 | 241 | if trimmedValue == "" { 242 | continue 243 | } 244 | 245 | if trimmedValue != "gzip" { 246 | // According to RFC 7231, Section 3.1.2.2: 247 | // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.2 248 | // An origin server MAY respond with a status code of 415 (Unsupported 249 | // Media Type) if a representation in the request message has a content 250 | // coding that is not acceptable. 251 | _ = c.AbortWithError(http.StatusUnsupportedMediaType, ErrUnsupportedContentEncoding) 252 | } 253 | 254 | r, err := gzip.NewReader(c.Request.Body) 255 | if err != nil { 256 | _ = c.AbortWithError(http.StatusBadRequest, err) 257 | 258 | return 259 | } 260 | 261 | toClose = append(toClose, c.Request.Body) 262 | 263 | c.Request.Body = r 264 | } 265 | 266 | c.Request.Header.Del("Content-Encoding") 267 | c.Request.Header.Del("Content-Length") 268 | 269 | c.Next() 270 | } 271 | --------------------------------------------------------------------------------