diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6f8288d5..864787ca 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -30,7 +30,7 @@ func main() { ``` -$ curl http://localhost:8201/hello/world +$ curl http://localhost:9000/hello/world Hello world ``` @@ -38,7 +38,7 @@ Hello world ``` -$ curl -i http://localhost:8201/hello/world +$ curl -i http://localhost:9000/hello/world ``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e27022d1..9a4c40d7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,12 +7,12 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - - cron: '0 17 * * 5' + - cron: "0 17 * * 5" jobs: analyze: @@ -29,15 +29,15 @@ jobs: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] # TODO: Enable for javascript later - language: [ 'go'] + language: ["go"] steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -46,4 +46,4 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index b758c7fa..b36e1010 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -16,23 +16,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: '^1.18' + go-version: "^1.18" - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup golangci-lint - uses: golangci/golangci-lint-action@v3.4.0 + uses: golangci/golangci-lint-action@v3.7.0 with: - version: v1.52.2 + version: v1.55.2 args: --verbose test: needs: lint strategy: matrix: os: [ubuntu-latest, macos-latest] - go: ['1.18', '1.19', '1.20'] - test-tags: ['', '-tags nomsgpack', '-tags "sonic avx"', '-tags go_json'] + go: ["1.18", "1.19", "1.20", "1.21"] + test-tags: ["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json"] include: - os: ubuntu-latest go-build: ~/.cache/go-build @@ -46,16 +46,17 @@ jobs: GOPROXY: https://proxy.golang.org steps: - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} + cache: false - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.ref }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ${{ matrix.go-build }} @@ -73,5 +74,5 @@ jobs: flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} - name: Format - if: matrix.go-version == '1.20.x' + if: matrix.go-version == '1.21.x' run: diff -u <(echo -n) <(gofmt -d .) diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index baf02af5..cbd5d418 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -3,7 +3,7 @@ name: Goreleaser on: push: tags: - - '*' + - "*" permissions: contents: write @@ -12,23 +12,22 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.20 + go-version: "^1" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@v5 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser version: latest - args: release --rm-dist + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index 91dae02c..4a72f734 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,6 @@ run: linters: enable: - asciicheck - - depguard - dogsled - durationcheck - errcheck diff --git a/CHANGELOG.md b/CHANGELOG.md index cf24ec28..79685205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Gin ChangeLog +## Gin v1.9.1 + +### BUG FIXES + +* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512) + +### SECURITY + +* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556) + +### ENHANCEMENTS + +* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455) +* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344) +* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525) + +### DOCS + +* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575) +* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583) + ## Gin v1.9.0 ### BREAK CHANGES diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 540bbbb8..77a1bde6 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -7,6 +7,7 @@ package binding import ( "errors" "fmt" + "mime/multipart" "reflect" "strconv" "strings" @@ -235,10 +236,17 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel switch value.Interface().(type) { case time.Time: return setTimeField(val, field, value) + case multipart.FileHeader: + return nil } return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) case reflect.Map: return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) + case reflect.Ptr: + if !value.Elem().IsValid() { + value.Set(reflect.New(value.Type().Elem())) + } + return setWithProperType(val, value.Elem(), field) default: return errUnknownType } diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 93d6a92f..16527eb9 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -5,6 +5,7 @@ package binding import ( + "mime/multipart" "reflect" "testing" "time" @@ -43,6 +44,7 @@ func TestMappingBaseTypes(t *testing.T) { {"zero value", struct{ F uint }{}, "", uint(0)}, {"zero value", struct{ F bool }{}, "", false}, {"zero value", struct{ F float32 }{}, "", float32(0)}, + {"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}}, } { tp := reflect.TypeOf(tt.value) testName := tt.name + ":" + tp.Field(0).Type.String() @@ -269,6 +271,39 @@ func TestMappingStructField(t *testing.T) { assert.Equal(t, 9, s.J.I) } +func TestMappingPtrField(t *testing.T) { + type ptrStruct struct { + Key int64 `json:"key"` + } + + type ptrRequest struct { + Items []*ptrStruct `json:"items" form:"items"` + } + + var err error + + // With 0 items. + var req0 ptrRequest + err = mappingByPtr(&req0, formSource{}, "form") + assert.NoError(t, err) + assert.Empty(t, req0.Items) + + // With 1 item. + var req1 ptrRequest + err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form") + assert.NoError(t, err) + assert.Len(t, req1.Items, 1) + assert.EqualValues(t, 1, req1.Items[0].Key) + + // With 2 items. + var req2 ptrRequest + err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form") + assert.NoError(t, err) + assert.Len(t, req2.Items, 2) + assert.EqualValues(t, 1, req2.Items[0].Key) + assert.EqualValues(t, 2, req2.Items[1].Key) +} + func TestMappingMapField(t *testing.T) { var s struct { M map[string]int diff --git a/context.go b/context.go index 5716318e..420ff167 100644 --- a/context.go +++ b/context.go @@ -1052,11 +1052,17 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { http.FileServer(fs).ServeHTTP(c.Writer, c.Request) } +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + // FileAttachment writes the specified file into the body stream in an efficient way // On the client side, the file will typically be downloaded with the given filename func (c *Context) FileAttachment(filepath, filename string) { if isASCII(filename) { - c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) + c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`) } else { c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) } @@ -1174,9 +1180,16 @@ func (c *Context) SetAccepted(formats ...string) { /***** GOLANG.ORG/X/NET/CONTEXT *****/ /************************************/ +// hasRequestContext returns whether c.Request has Context and fallback. +func (c *Context) hasRequestContext() bool { + hasFallback := c.engine != nil && c.engine.ContextWithFallback + hasRequestContext := c.Request != nil && c.Request.Context() != nil + return hasFallback && hasRequestContext +} + // Deadline returns that there is no deadline (ok==false) when c.Request has no Context. func (c *Context) Deadline() (deadline time.Time, ok bool) { - if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { + if !c.hasRequestContext() { return } return c.Request.Context().Deadline() @@ -1184,7 +1197,7 @@ func (c *Context) Deadline() (deadline time.Time, ok bool) { // Done returns nil (chan which will wait forever) when c.Request has no Context. func (c *Context) Done() <-chan struct{} { - if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { + if !c.hasRequestContext() { return nil } return c.Request.Context().Done() @@ -1192,7 +1205,7 @@ func (c *Context) Done() <-chan struct{} { // Err returns nil when c.Request has no Context. func (c *Context) Err() error { - if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { + if !c.hasRequestContext() { return nil } return c.Request.Context().Err() @@ -1213,7 +1226,7 @@ func (c *Context) Value(key any) any { return val } } - if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil { + if !c.hasRequestContext() { return nil } return c.Request.Context().Value(key) diff --git a/context_test.go b/context_test.go index 1dec902c..70d47583 100644 --- a/context_test.go +++ b/context_test.go @@ -1032,6 +1032,20 @@ func TestContextRenderAttachment(t *testing.T) { assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.Header().Get("Content-Disposition")) } +func TestContextRenderAndEscapeAttachment(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + maliciousFilename := "tampering_field.sh\"; \\\"; dummy=.go" + actualEscapedResponseFilename := "tampering_field.sh\\\"; \\\\\\\"; dummy=.go" + + c.Request, _ = http.NewRequest("GET", "/", nil) + c.FileAttachment("./gin.go", maliciousFilename) + + assert.Equal(t, 200, w.Code) + assert.Contains(t, w.Body.String(), "func New() *Engine {") + assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", actualEscapedResponseFilename), w.Header().Get("Content-Disposition")) +} + func TestContextRenderUTF8Attachment(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -2162,6 +2176,24 @@ func TestRemoteIPFail(t *testing.T) { assert.False(t, trust) } +func TestHasRequestContext(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + assert.False(t, c.hasRequestContext(), "no request, no fallback") + c.engine.ContextWithFallback = true + assert.False(t, c.hasRequestContext(), "no request, has fallback") + c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) + assert.True(t, c.hasRequestContext(), "has request, has fallback") + c.Request, _ = http.NewRequestWithContext(nil, "", "", nil) //nolint:staticcheck + assert.False(t, c.hasRequestContext(), "has request with nil ctx, has fallback") + c.engine.ContextWithFallback = false + assert.False(t, c.hasRequestContext(), "has request, no fallback") + + c = &Context{} + assert.False(t, c.hasRequestContext(), "no request, no engine") + c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) + assert.False(t, c.hasRequestContext(), "has request, no engine") +} + func TestContextWithFallbackDeadlineFromRequestContext(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) // enable ContextWithFallback feature flag diff --git a/docs/doc.md b/docs/doc.md index e48c2ba1..520d105c 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -508,6 +508,44 @@ Sample Output ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ``` +### Skip logging + +```go +func main() { + router := gin.New() + + // skip logging for desired paths by setting SkipPaths in LoggerConfig + loggerConfig := gin.LoggerConfig{SkipPaths: []string{"/metrics"}} + + // skip logging based on your logic by setting Skip func in LoggerConfig + loggerConfig.Skip = func(c *gin.Context) bool { + // as an example skip non server side errors + return c.Writer.Status() < http.StatusInternalServerError + } + + engine.Use(gin.LoggerWithConfig(loggerConfig)) + router.Use(gin.Recovery()) + + // skipped + router.GET("/metrics", func(c *gin.Context) { + c.Status(http.StatusNotImplemented) + }) + + // skipped + router.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "pong") + }) + + // not skipped + router.GET("/data", func(c *gin.Context) { + c.Status(http.StatusNotImplemented) + }) + + router.Run(":8080") +} + +``` + ### Controlling Log output coloring By default, logs output on console should be colorized depending on the detected TTY. diff --git a/gin.go b/gin.go index ed8b6dad..5a605cf1 100644 --- a/gin.go +++ b/gin.go @@ -334,7 +334,6 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { } root.addRoute(path, handlers) - // Update maxParams if paramsCount := countParams(path); paramsCount > engine.maxParams { engine.maxParams = paramsCount } diff --git a/go.mod b/go.mod index 5fa62004..0b60c5d7 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,35 @@ module github.com/gin-gonic/gin go 1.20 require ( - github.com/bytedance/sonic v1.8.8 + github.com/bytedance/sonic v1.10.2 github.com/gin-contrib/sse v0.1.0 - github.com/go-playground/validator/v10 v10.12.0 + github.com/go-playground/validator/v10 v10.17.0 github.com/goccy/go-json v0.10.2 github.com/json-iterator/go v1.1.12 - github.com/mattn/go-isatty v0.0.18 - github.com/pelletier/go-toml/v2 v2.0.7 - github.com/stretchr/testify v1.8.2 - github.com/ugorji/go/codec v1.2.11 - golang.org/x/net v0.9.0 - google.golang.org/protobuf v1.30.0 + github.com/mattn/go-isatty v0.0.20 + github.com/pelletier/go-toml/v2 v2.1.1 + github.com/stretchr/testify v1.8.4 + github.com/ugorji/go/codec v1.2.12 + golang.org/x/net v0.20.0 + google.golang.org/protobuf v1.32.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.3.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.8.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 90704f4e..e360d9d2 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,19 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= -github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -14,30 +21,29 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= -github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= -github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.3.0 h1:jX8FDLfW4ThVXctBNZ+3cIWnCSnrACDV73r76dy0aQQ= +github.com/leodido/go-urn v1.3.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= -github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -48,33 +54,32 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/logger.go b/logger.go index cd1e7fa6..db2c6832 100644 --- a/logger.go +++ b/logger.go @@ -47,8 +47,15 @@ type LoggerConfig struct { // SkipPaths is an url path array which logs are not written. // Optional. SkipPaths []string + + // Skip is a Skipper that indicates which logs should not be written. + // Optional. + Skip Skipper } +// Skipper is a function to skip logs based on provided Context +type Skipper func(c *Context) bool + // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter type LogFormatter func(params LogFormatterParams) string @@ -83,6 +90,8 @@ func (p *LogFormatterParams) StatusCodeColor() string { code := p.StatusCode switch { + case code >= http.StatusContinue && code < http.StatusOK: + return white case code >= http.StatusOK && code < http.StatusMultipleChoices: return green case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: @@ -239,32 +248,34 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { // Process request c.Next() - // Log only when path is not being skipped - if _, ok := skip[path]; !ok { - param := LogFormatterParams{ - Request: c.Request, - isTerm: isTerm, - Keys: c.Keys, - } - - // Stop timer - param.TimeStamp = time.Now() - param.Latency = param.TimeStamp.Sub(start) - - param.ClientIP = c.ClientIP() - param.Method = c.Request.Method - param.StatusCode = c.Writer.Status() - param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() - - param.BodySize = c.Writer.Size() - - if raw != "" { - path = path + "?" + raw - } - - param.Path = path - - fmt.Fprint(out, formatter(param)) + // Log only when it is not being skipped + if _, ok := skip[path]; ok || (conf.Skip != nil && conf.Skip(c)) { + return } + + param := LogFormatterParams{ + Request: c.Request, + isTerm: isTerm, + Keys: c.Keys, + } + + // Stop timer + param.TimeStamp = time.Now() + param.Latency = param.TimeStamp.Sub(start) + + param.ClientIP = c.ClientIP() + param.Method = c.Request.Method + param.StatusCode = c.Writer.Status() + param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() + + param.BodySize = c.Writer.Size() + + if raw != "" { + path = path + "?" + raw + } + + param.Path = path + + fmt.Fprint(out, formatter(param)) } } diff --git a/logger_test.go b/logger_test.go index 5f78708f..6c1814dc 100644 --- a/logger_test.go +++ b/logger_test.go @@ -310,6 +310,7 @@ func TestColorForStatus(t *testing.T) { return p.StatusCodeColor() } + assert.Equal(t, white, colorForStatus(http.StatusContinue), "1xx should be white") assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green") assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white") assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow") @@ -414,6 +415,26 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) { assert.Contains(t, buffer.String(), "") } +func TestLoggerWithConfigSkipper(t *testing.T) { + buffer := new(strings.Builder) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + Skip: func(c *Context) bool { + return c.Writer.Status() == http.StatusNoContent + }, + })) + router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) }) + router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) }) + + PerformRequest(router, "GET", "/logged") + assert.Contains(t, buffer.String(), "200") + + buffer.Reset() + PerformRequest(router, "GET", "/skipped") + assert.Contains(t, buffer.String(), "") +} + func TestDisableConsoleColor(t *testing.T) { New() assert.Equal(t, autoColor, consoleColorMode) diff --git a/render/render_test.go b/render/render_test.go index 86dc362d..c9db635f 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -578,3 +578,16 @@ func TestRenderReaderNoContentLength(t *testing.T) { assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) } + +func TestRenderWriteError(t *testing.T) { + data := []interface{}{"value1", "value2"} + prefix := "my-prefix:" + r := SecureJSON{Data: data, Prefix: prefix} + ew := &errorWriter{ + bufString: prefix, + ResponseRecorder: httptest.NewRecorder(), + } + err := r.Render(ew) + assert.NotNil(t, err) + assert.Equal(t, `write "my-prefix:" error`, err.Error()) +} diff --git a/response_writer_test.go b/response_writer_test.go index 9fd5e87c..964aa307 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -156,3 +156,33 @@ func TestResponseWriterStatusCode(t *testing.T) { // status must be 200 although we tried to change it assert.Equal(t, http.StatusOK, w.Status()) } + +// mockPusherResponseWriter is an http.ResponseWriter that implements http.Pusher. +type mockPusherResponseWriter struct { + http.ResponseWriter +} + +func (m *mockPusherResponseWriter) Push(target string, opts *http.PushOptions) error { + return nil +} + +// nonPusherResponseWriter is an http.ResponseWriter that does not implement http.Pusher. +type nonPusherResponseWriter struct { + http.ResponseWriter +} + +func TestPusherWithPusher(t *testing.T) { + rw := &mockPusherResponseWriter{} + w := &responseWriter{ResponseWriter: rw} + + pusher := w.Pusher() + assert.NotNil(t, pusher, "Expected pusher to be non-nil") +} + +func TestPusherWithoutPusher(t *testing.T) { + rw := &nonPusherResponseWriter{} + w := &responseWriter{ResponseWriter: rw} + + pusher := w.Pusher() + assert.Nil(t, pusher, "Expected pusher to be nil") +} diff --git a/routes_test.go b/routes_test.go index 633c0aba..7a51f817 100644 --- a/routes_test.go +++ b/routes_test.go @@ -337,6 +337,45 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) { assert.Equal(t, "/is/super/great", wild) } +// TestRouteParamsNotEmpty tests that context parameters will be set +// even if a route with params/wildcards is registered after the context +// initialisation (which happened in a previous requets). +func TestRouteParamsNotEmpty(t *testing.T) { + name := "" + lastName := "" + wild := "" + router := New() + + w := PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great") + + assert.Equal(t, http.StatusNotFound, w.Code) + + router.GET("/test/:name/:last_name/*wild", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + var ok bool + wild, ok = c.Params.Get("wild") + + assert.True(t, ok) + assert.Equal(t, name, c.Param("name")) + assert.Equal(t, lastName, c.Param("last_name")) + + assert.Empty(t, c.Param("wtf")) + assert.Empty(t, c.Params.ByName("wtf")) + + wtf, ok := c.Params.Get("wtf") + assert.Empty(t, wtf) + assert.False(t, ok) + }) + + w = PerformRequest(router, http.MethodGet, "/test/john/smith/is/super/great") + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "john", name) + assert.Equal(t, "smith", lastName) + assert.Equal(t, "/is/super/great", wild) +} + // TestHandleStaticFile - ensure the static file handles properly func TestRouteStaticFile(t *testing.T) { // SETUP file diff --git a/tree.go b/tree.go index dda8f4f7..878023d1 100644 --- a/tree.go +++ b/tree.go @@ -351,7 +351,10 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) } if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0] + pathSeg := "" + if len(n.children) != 0 { + pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0] + } panic("catch-all wildcard '" + path + "' in new path '" + fullPath + "' conflicts with existing path segment '" + pathSeg + @@ -478,7 +481,7 @@ walk: // Outer loop for walking the tree // We can recommend to redirect to the same URL without a // trailing slash if a leaf exists for that path. value.tsr = path == "/" && n.handlers != nil - return + return value } // Handle wildcard child, which is always at the end of the array @@ -497,7 +500,14 @@ walk: // Outer loop for walking the tree } // Save param value - if params != nil && cap(*params) > 0 { + if params != nil { + // Preallocate capacity if necessary + if cap(*params) < int(globalParamsCount) { + newParams := make(Params, len(*params), globalParamsCount) + copy(newParams, *params) + *params = newParams + } + if value.params == nil { value.params = params } @@ -526,12 +536,12 @@ walk: // Outer loop for walking the tree // ... but we can't value.tsr = len(path) == end+1 - return + return value } if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath - return + return value } if len(n.children) == 1 { // No handle found. Check if a handle for this path + a @@ -539,11 +549,18 @@ walk: // Outer loop for walking the tree n = n.children[0] value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") } - return + return value case catchAll: // Save param value if params != nil { + // Preallocate capacity if necessary + if cap(*params) < int(globalParamsCount) { + newParams := make(Params, len(*params), globalParamsCount) + copy(newParams, *params) + *params = newParams + } + if value.params == nil { value.params = params } @@ -564,7 +581,7 @@ walk: // Outer loop for walking the tree value.handlers = n.handlers value.fullPath = n.fullPath - return + return value default: panic("invalid node type") @@ -595,7 +612,7 @@ walk: // Outer loop for walking the tree // Check if this node has a handle registered. if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath - return + return value } // If there is no handle for this route, but this route has a @@ -603,12 +620,12 @@ walk: // Outer loop for walking the tree // additional trailing slash if path == "/" && n.wildChild && n.nType != root { value.tsr = true - return + return value } if path == "/" && n.nType == static { value.tsr = true - return + return value } // No handle found. Check if a handle for this path + a @@ -618,11 +635,11 @@ walk: // Outer loop for walking the tree n = n.children[i] value.tsr = (len(n.path) == 1 && n.handlers != nil) || (n.nType == catchAll && n.children[0].handlers != nil) - return + return value } } - return + return value } // Nothing found. We can recommend to redirect to the same URL with an @@ -648,7 +665,7 @@ walk: // Outer loop for walking the tree } } - return + return value } } diff --git a/tree_test.go b/tree_test.go index 2005738e..c9b03130 100644 --- a/tree_test.go +++ b/tree_test.go @@ -417,6 +417,8 @@ func TestTreeWildcardConflict(t *testing.T) { {"/user_:name", false}, {"/id:id", false}, {"/id/:id", false}, + {"/static/*file", false}, + {"/static/", true}, } testRoutes(t, routes) } @@ -893,9 +895,9 @@ func TestTreeInvalidNodeType(t *testing.T) { func TestTreeInvalidParamsType(t *testing.T) { tree := &node{} - tree.wildChild = true - tree.children = append(tree.children, &node{}) - tree.children[0].nType = 2 + // add a child with wildcard + route := "/:path" + tree.addRoute(route, fakeHandler(route)) // set invalid Params type params := make(Params, 0) @@ -904,6 +906,34 @@ func TestTreeInvalidParamsType(t *testing.T) { tree.getValue("/test", ¶ms, getSkippedNodes(), false) } +func TestTreeExpandParamsCapacity(t *testing.T) { + data := []struct { + path string + }{ + {"/:path"}, + {"/*path"}, + } + + for _, item := range data { + tree := &node{} + tree.addRoute(item.path, fakeHandler(item.path)) + params := make(Params, 0) + + value := tree.getValue("/test", ¶ms, getSkippedNodes(), false) + + if value.params == nil { + t.Errorf("Expected %s params to be set, but they weren't", item.path) + continue + } + + if len(*value.params) != 1 { + t.Errorf("Wrong number of %s params: got %d, want %d", + item.path, len(*value.params), 1) + continue + } + } +} + func TestTreeWildcardConflictEx(t *testing.T) { conflicts := [...]struct { route string diff --git a/version.go b/version.go index 390da4f3..85462e55 100644 --- a/version.go +++ b/version.go @@ -5,4 +5,4 @@ package gin // Version is the current gin framework's version. -const Version = "v1.9.0" +const Version = "v1.9.1"