diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index 8ece7f1d..d909d22d 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -26,14 +26,14 @@ jobs: - name: Setup golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.6 + version: v2.9 args: --verbose test: needs: lint strategy: matrix: os: [ubuntu-latest, macos-latest] - go: ["1.24", "1.25"] + go: ["1.25", "1.26"] test-tags: [ "", diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 0098b952..ea933e7e 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: with: go-version: "^1" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index b86aed7f..a4c62bf4 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -9,7 +9,7 @@ on: - master schedule: # Run daily at 00:00 UTC - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: # Allow manual trigger permissions: @@ -27,30 +27,30 @@ jobs: fetch-depth: 0 - name: Run Trivy vulnerability scanner (source code) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.1 with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - name: Upload Trivy results to GitHub Security tab uses: github/codeql-action/upload-sarif@v4 if: always() with: - sarif_file: 'trivy-results.sarif' + sarif_file: "trivy-results.sarif" - name: Run Trivy scanner (table output for logs) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.1 if: always() with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'table' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - exit-code: '1' + exit-code: "1" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9451db39..95bc2f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,74 +1,123 @@ # Gin ChangeLog +## Gin v1.12.0 + +### Features + +- feat(render): add bson protocol ([#4145](https://github.com/gin-gonic/gin/pull/4145)) +- feat(context): add GetError and GetErrorSlice methods for error retrieval ([#4502](https://github.com/gin-gonic/gin/pull/4502)) +- feat(binding): add support for encoding.UnmarshalText in uri/query binding ([#4203](https://github.com/gin-gonic/gin/pull/4203)) +- feat(gin): add option to use escaped path ([#4420](https://github.com/gin-gonic/gin/pull/4420)) +- feat(context): add Protocol Buffers support to content negotiation ([#4423](https://github.com/gin-gonic/gin/pull/4423)) +- feat(context): implemented Delete method ([#38e7651](https://github.com/gin-gonic/gin/commit/38e7651)) +- feat(logger): color latency ([#4146](https://github.com/gin-gonic/gin/pull/4146)) + +### Enhancements + +- perf(tree): reduce allocations in findCaseInsensitivePath ([#4417](https://github.com/gin-gonic/gin/pull/4417)) +- perf(recovery): optimize line reading in stack function ([#4466](https://github.com/gin-gonic/gin/pull/4466)) +- perf(path): replace regex with custom functions in redirectTrailingSlash ([#4414](https://github.com/gin-gonic/gin/pull/4414)) +- perf(tree): optimize path parsing using strings.Count ([#4246](https://github.com/gin-gonic/gin/pull/4246)) +- chore(logger): allow skipping query string output ([#4547](https://github.com/gin-gonic/gin/pull/4547)) +- chore(context): always trust xff headers from unix socket ([#3359](https://github.com/gin-gonic/gin/pull/3359)) +- chore(response): prevent Flush() panic when the underlying ResponseWriter does not implement `http.Flusher` ([#4479](https://github.com/gin-gonic/gin/pull/4479)) +- refactor(recovery): smart error comparison ([#4142](https://github.com/gin-gonic/gin/pull/4142)) +- refactor(context): replace hardcoded localhost IPs with constants ([#4481](https://github.com/gin-gonic/gin/pull/4481)) +- refactor(utils): move util functions to utils.go ([#4467](https://github.com/gin-gonic/gin/pull/4467)) +- refactor(binding): use maps.Copy for cleaner map handling ([#4352](https://github.com/gin-gonic/gin/pull/4352)) +- refactor(context): using maps.Clone ([#4333](https://github.com/gin-gonic/gin/pull/4333)) +- refactor(ginS): use sync.OnceValue to simplify engine function ([#4314](https://github.com/gin-gonic/gin/pull/4314)) +- refactor: replace magic numbers with named constants in bodyAllowedForStatus ([#4529](https://github.com/gin-gonic/gin/pull/4529)) +- refactor: for loop can be modernized using range over int ([#4392](https://github.com/gin-gonic/gin/pull/4392)) + +### Bug Fixes + +- fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath ([#4535](https://github.com/gin-gonic/gin/pull/4535)) +- fix(render): write content length in Data.Render ([#4206](https://github.com/gin-gonic/gin/pull/4206)) +- fix(context): ClientIP handling for multiple X-Forwarded-For header values ([#4472](https://github.com/gin-gonic/gin/pull/4472)) +- fix(binding): empty value error ([#2169](https://github.com/gin-gonic/gin/pull/2169)) +- fix(recover): suppress http.ErrAbortHandler in recover ([#4336](https://github.com/gin-gonic/gin/pull/4336)) +- fix(gin): literal colon routes not working with engine.Handler() ([#4415](https://github.com/gin-gonic/gin/pull/4415)) +- fix(gin): close os.File in RunFd to prevent resource leak ([#4422](https://github.com/gin-gonic/gin/pull/4422)) +- fix(response): refine hijack behavior for response lifecycle ([#4373](https://github.com/gin-gonic/gin/pull/4373)) +- fix(binding): improve empty slice/array handling in form binding ([#4380](https://github.com/gin-gonic/gin/pull/4380)) +- fix(debug): version mismatch ([#4403](https://github.com/gin-gonic/gin/pull/4403)) +- fix: correct typos, improve documentation clarity, and remove dead code ([#4511](https://github.com/gin-gonic/gin/pull/4511)) + +### Build process updates / CI + +- ci: update Go version support to 1.25+ across CI and docs ([#4550](https://github.com/gin-gonic/gin/pull/4550)) +- chore(binding): upgrade bson dependency to mongo-driver v2 ([#4549](https://github.com/gin-gonic/gin/pull/4549)) + ## Gin v1.11.0 ### Features -* feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210)) -* feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970)) -* feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904)) -* feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939)) -* feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190)) -* feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048)) -* feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633)) +- feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210)) +- feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970)) +- feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904)) +- feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939)) +- feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190)) +- feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048)) +- feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633)) ### Enhancements -* perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339)) -* refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354)) -* refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353)) -* refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080)) -* refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910)) -* refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181)) -* refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066)) -* refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522)) -* refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068)) -* refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969)) -* refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966)) -* tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975)) -* context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181)) +- perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339)) +- refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354)) +- refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353)) +- refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080)) +- refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910)) +- refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181)) +- refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066)) +- refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522)) +- refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068)) +- refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969)) +- refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966)) +- tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975)) +- context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181)) ### Bug Fixes -* fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987)) -* fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193)) -* fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003)) -* fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580)) -* fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969)) -* fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966)) -* fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413)) -* fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222)) -* fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077)) +- fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987)) +- fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193)) +- fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003)) +- fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580)) +- fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969)) +- fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966)) +- fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413)) +- fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222)) +- fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077)) ### Build process updates / CI -* ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359)) -* ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341)) -* build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342)) -* ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154)) -* build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960)) -* ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090)) -* ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126)) +- ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359)) +- ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341)) +- build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342)) +- ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154)) +- build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960)) +- ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090)) +- ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126)) ### Dependency updates -* chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356)) -* chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347)) -* chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351)) -* chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328)) -* chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221)) -* chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052)) +- chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356)) +- chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347)) +- chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351)) +- chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328)) +- chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221)) +- chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052)) ### Documentation updates -* docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360)) -* docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207)) -* docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953)) -* docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997)) -* docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938)) -* docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020)) -* docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078)) -* docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079)) +- docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360)) +- docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207)) +- docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953)) +- docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997)) +- docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938)) +- docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020)) +- docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078)) +- docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079)) --- @@ -76,377 +125,377 @@ ### Features -* refactor: strengthen HTTPS security and improve code organization -* feat(binding): Support custom BindUnmarshaler for binding. (#3933) +- refactor: strengthen HTTPS security and improve code organization +- feat(binding): Support custom BindUnmarshaler for binding. (#3933) ### Enhancements -* chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940) -* chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941) -* chore: update external dependencies to latest versions (#3950) -* chore: update various Go dependencies to latest versions (#3901) -* chore: refactor configuration files for better readability (#3951) -* chore: update changelog categories and improve documentation (#3917) -* feat: update version constant to v1.10.0 (#3952) +- chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940) +- chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941) +- chore: update external dependencies to latest versions (#3950) +- chore: update various Go dependencies to latest versions (#3901) +- chore: refactor configuration files for better readability (#3951) +- chore: update changelog categories and improve documentation (#3917) +- feat: update version constant to v1.10.0 (#3952) ### Build process updates -* ci(release): refactor changelog regex patterns and exclusions (#3914) -* ci(Makefile): vet command add .PHONY (#3915) +- ci(release): refactor changelog regex patterns and exclusions (#3914) +- ci(Makefile): vet command add .PHONY (#3915) ## Gin v1.10.0 ### Features -* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1) -* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost) -* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb) -* feat(binding): support override default binding implement (#3514) (@ssfyn) -* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125) -* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh) +- feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1) +- feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost) +- feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb) +- feat(binding): support override default binding implement (#3514) (@ssfyn) +- feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125) +- feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh) ### Bug fixes -* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy) -* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn) -* fix(binding): dereference pointer to struct (#3199) (@echovl) -* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax) -* fix(engine): fix unit test (#3878) (@flc1125) -* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon) -* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli) -* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32) -* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption) -* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3) -* fix(uri): query binding bug (#3236) (@illiafox) -* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss) -* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish) +- Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy) +- fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn) +- fix(binding): dereference pointer to struct (#3199) (@echovl) +- fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax) +- fix(engine): fix unit test (#3878) (@flc1125) +- fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon) +- fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli) +- fix(router): catch-all conflicting wildcard (#3812) (@FirePing32) +- fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption) +- fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3) +- fix(uri): query binding bug (#3236) (@illiafox) +- fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss) +- fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish) ### Enhancements -* chore(CI): update release args (#3595) (@qloog) -* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab) -* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez) -* chore(deps): update dependencies to latest versions (#3835) (@appleboy) -* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat) -* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme) -* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538) -* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538) -* chore(refactor): modify interface check way (#3855) (@demoManito) -* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024) -* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz) -* chore: refactor CI and update dependencies (#3848) (@appleboy) -* chore: refactor configuration files for better readability (#3951) (@appleboy) -* chore: update GitHub Actions configuration (#3792) (@appleboy) -* chore: update changelog categories and improve documentation (#3917) (@appleboy) -* chore: update dependencies to latest versions (#3694) (@appleboy) -* chore: update external dependencies to latest versions (#3950) (@appleboy) -* chore: update various Go dependencies to latest versions (#3901) (@appleboy) +- chore(CI): update release args (#3595) (@qloog) +- chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab) +- chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez) +- chore(deps): update dependencies to latest versions (#3835) (@appleboy) +- chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat) +- chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme) +- chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538) +- chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538) +- chore(refactor): modify interface check way (#3855) (@demoManito) +- chore(request): check reader if it's nil before reading (#3419) (@noahyao1024) +- chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz) +- chore: refactor CI and update dependencies (#3848) (@appleboy) +- chore: refactor configuration files for better readability (#3951) (@appleboy) +- chore: update GitHub Actions configuration (#3792) (@appleboy) +- chore: update changelog categories and improve documentation (#3917) (@appleboy) +- chore: update dependencies to latest versions (#3694) (@appleboy) +- chore: update external dependencies to latest versions (#3950) (@appleboy) +- chore: update various Go dependencies to latest versions (#3901) (@appleboy) ### Build process updates -* build(codecov): Added a codecov configuration (#3891) (@flc1125) -* ci(Makefile): vet command add .PHONY (#3915) (@imalasong) -* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy) -* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy) -* ci(testing): add go1.22 version (#3842) (@appleboy) +- build(codecov): Added a codecov configuration (#3891) (@flc1125) +- ci(Makefile): vet command add .PHONY (#3915) (@imalasong) +- ci(lint): update tooling and workflows for consistency (#3834) (@appleboy) +- ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy) +- ci(testing): add go1.22 version (#3842) (@appleboy) ### Documentation updates -* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125) -* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1) -* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1) -* docs: fix typo in comment (#3868) (@testwill) -* docs: fix typo in function documentation (#3872) (@TotomiEcio) -* docs: remove redundant comments (#3765) (@WeiTheShinobi) -* feat: update version constant to v1.10.0 (#3952) (@appleboy) +- docs(context): Added deprecation comments to BindWith (#3880) (@flc1125) +- docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1) +- docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1) +- docs: fix typo in comment (#3868) (@testwill) +- docs: fix typo in function documentation (#3872) (@TotomiEcio) +- docs: remove redundant comments (#3765) (@WeiTheShinobi) +- feat: update version constant to v1.10.0 (#3952) (@appleboy) ### Others -* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf) -* test(git): gitignore add develop tools (#3370) (@demoManito) -* test(http): use constant instead of numeric literal (#3863) (@testwill) -* test(path): Optimize unit test execution results (#3883) (@flc1125) -* test(render): increased unit tests coverage (#3691) (@araujo88) +- Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf) +- test(git): gitignore add develop tools (#3370) (@demoManito) +- test(http): use constant instead of numeric literal (#3863) (@testwill) +- test(path): Optimize unit test execution results (#3883) (@flc1125) +- test(render): increased unit tests coverage (#3691) (@araujo88) ## Gin v1.9.1 ### BUG FIXES -* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512) +- 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) +- 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) +- 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) +- 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 -* Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150) +- Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150) ### BUG FIXES -* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460) -* fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397) -* Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503) +- fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460) +- fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397) +- Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503) ### SECURITY -* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333) -* fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505) +- Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333) +- fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505) ### ENHANCEMENTS -* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184) -* chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316) -* fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327) -* remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395) -* refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433) -* console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453) -* chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456) -* chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464) -* chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489) +- feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184) +- chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316) +- fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327) +- remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395) +- refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433) +- console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453) +- chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456) +- chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464) +- chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489) ### DOCS -* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260) -* docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400) -* docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449) -* docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446) +- docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260) +- docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400) +- docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449) +- docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446) ## Gin v1.8.2 ### BUG FIXES -* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227))) -* fix(engine): missing route params for CreateTestContext ([#2778]((https://github.com/gin-gonic/gin/pull/2778))) ([#2803]((https://github.com/gin-gonic/gin/pull/2803))) +- fix(route): redirectSlash bug ([#3227](<(https://github.com/gin-gonic/gin/pull/3227)>)) +- fix(engine): missing route params for CreateTestContext ([#2778](<(https://github.com/gin-gonic/gin/pull/2778)>)) ([#2803](<(https://github.com/gin-gonic/gin/pull/2803)>)) ### SECURITY -* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432))) +- Fix the GO-2022-1144 vulnerability ([#3432](<(https://github.com/gin-gonic/gin/pull/3432)>)) ## Gin v1.8.1 ### ENHANCEMENTS -* feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172) +- feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172) ## Gin v1.8.0 ### BREAK CHANGES -* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP` -* gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751) +- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP` +- gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751) ### BUG FIXES -* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861) -* Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983) -* Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123) +- Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861) +- Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983) +- Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123) ### ENHANCEMENTS -* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694) -* RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685) -* Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680) -* Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707) -* Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711) -* Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723) -* Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files) -* Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737) -* Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765) -* Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720) -* Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787) -* Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769) -* Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701) -* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967) -* Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012) -* Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398) -* Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071) -* Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749) -* Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825) -* Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139) -* Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081) -* IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033) +- Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694) +- RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685) +- Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680) +- Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707) +- Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711) +- Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723) +- Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files) +- Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737) +- Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765) +- Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720) +- Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787) +- Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769) +- Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701) +- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967) +- Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012) +- Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398) +- Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071) +- Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749) +- Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825) +- Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139) +- Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081) +- IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033) ### DOCS -* Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703) +- Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703) ## Gin v1.7.7 ### BUG FIXES -* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862). -* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878). -* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843). -* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918). +- Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862). +- Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878). +- Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843). +- Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918). ### ENHANCEMENTS -* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819). -* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906). +- TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819). +- TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906). ### DOCS -* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)). +- NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)). ## Gin v1.7.6 ### BUG FIXES -* bump new release to fix v1.7.5 release error by using v1.7.4 codes. +- bump new release to fix v1.7.5 release error by using v1.7.4 codes. ## Gin v1.7.4 ### BUG FIXES -* bump new release to fix checksum mismatch +- bump new release to fix checksum mismatch ## Gin v1.7.3 ### BUG FIXES -* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796) +- fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796) ## Gin v1.7.2 ### BUG FIXES -* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696). +- Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696). ## Gin v1.7.1 ### BUG FIXES -* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675)) +- fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675)) ## Gin v1.7.0 ### BUG FIXES -* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600)) -* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528)) -* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366)) +- fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600)) +- fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528)) +- fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366)) ### ENHANCEMENTS -* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663)) -* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365)) -* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368)) -* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375)) -* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378)) -* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387)) -* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321)) -* remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391)) -* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389)) -* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322)) -* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450)) -* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412)) -* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487)) -* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512)) -* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508)) -* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526)) -* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484)) -* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371)) -* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302)) -* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609)) -* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663)) -* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632)) +- Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663)) +- chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365)) +- Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368)) +- chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375)) +- chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378)) +- Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387)) +- update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321)) +- remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391)) +- Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389)) +- Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322)) +- binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450)) +- Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412)) +- Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487)) +- update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512)) +- reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508)) +- implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526)) +- Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484)) +- chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371)) +- Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302)) +- basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609)) +- Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663)) +- feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632)) ## Gin v1.6.3 ### ENHANCEMENTS - * Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351) +- Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351) ## Gin v1.6.2 ### BUG FIXES - * fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305) +- fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305) ### ENHANCEMENTS - * Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306) +- Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306) ## Gin v1.6.1 ### BUG FIXES - * Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294) +- Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294) ## Gin v1.6.0 ### BREAKING - * chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159) - * drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148) - * Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615) +- chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159) +- drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148) +- Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615) ### FEATURES - * add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220) - * FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112) +- add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220) +- FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112) ### BUG FIXES - * Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280) - * Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228) - * fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216) - * Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166) - * [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121) - * Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391) +- Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280) +- Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228) +- fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216) +- Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166) +- [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121) +- Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391) ### ENHANCEMENTS - * Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277) - * tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229) - * tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222) - * chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215) - * path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212) - * Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206) - * Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179) - * tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177) - * tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171) - * tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163) - * use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155) - * upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149) - * Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970) - * Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852) +- Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277) +- tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229) +- tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222) +- chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215) +- path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212) +- Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206) +- Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179) +- tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177) +- tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171) +- tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163) +- use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155) +- upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149) +- Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970) +- Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852) ### DOCS - * docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223) - * Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217) - * Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202) - * Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198) - * Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196) - * Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190) - * upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189) - * Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188) - * Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186) - * Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165) - * docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153) - * Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122) +- docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223) +- Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217) +- Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202) +- Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198) +- Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196) +- Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190) +- upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189) +- Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188) +- Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186) +- Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165) +- docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153) +- Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122) ### MISC - * ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262) - * chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231) - * Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147) - * fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129) +- ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262) +- chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231) +- Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147) +- fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129) ## Gin v1.5.0 @@ -485,14 +534,14 @@ ### Gin v1.4.0 -- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) +- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) - [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) - [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797) - [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789) -- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804) +- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804) - [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779) - [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791) - [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794) @@ -524,7 +573,7 @@ - [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609) - [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618) - [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600) -- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559) +- [FIX] LoadHTML\* tests [#1559](https://github.com/gin-gonic/gin/pull/1559) - [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565) - [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571) - [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570) @@ -539,7 +588,6 @@ - [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485) - [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491) - ## Gin v1.3.0 - [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383) @@ -637,7 +685,6 @@ - [FIX] Error implements the json.Marshaller interface - [FIX] MIT license in every file - ## Gin 1.0rc1 (May 22, 2015) - [PERFORMANCE] Zero allocation router @@ -681,7 +728,6 @@ - [FIX] Hijacking http - [FIX] Better support for Google App Engine (using log instead of fmt) - ## Gin 0.6 (Mar 9, 2015) - [NEW] Support multipart/form-data @@ -691,14 +737,12 @@ - [FIX] Unsigned integers in binding - [FIX] Improve color logger - ## Gin 0.5 (Feb 7, 2015) - [NEW] Content Negotiation - [FIX] Solved security bug that allow a client to spoof ip - [FIX] Fix unexported/ignored fields in binding - ## Gin 0.4 (Aug 21, 2014) - [NEW] Development mode @@ -707,7 +751,6 @@ - [FIX] Deferring WriteHeader() - [FIX] Improved documentation for model binding - ## Gin 0.3 (Jul 18, 2014) - [PERFORMANCE] Normal log and error log are printed in the same call. @@ -725,8 +768,8 @@ - [FIX] Renaming Context.Req to Context.Request - [FIX] Check application/x-www-form-urlencoded when parsing form - ## Gin 0.2b (Jul 08, 2014) + - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead - [NEW] Travis CI integration - [NEW] Completely new logger diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9703d6b4..3b05a160 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,6 @@ Please ensure your pull request meets the following requirements: - All tests pass in available continuous integration systems (e.g., GitHub Actions). - Add or modify tests to cover your code changes. - If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README. -- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1). +- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md). Thank you for contributing! diff --git a/README.md b/README.md index 1b9ab808..d6492c06 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) -## ๐Ÿ“ฐ [Announcing Gin 1.11.0!](https://gin-gonic.com/en/blog/news/gin-1-11-0-release-announcement/) +## ๐Ÿ“ฐ Gin 1.12.0 is now available! -Read about the latest features and improvements in Gin 1.11.0 on our official blog. +We're excited to announce the release of **[Gin 1.12.0](https://gin-gonic.com/en/blog/news/gin-1-12-0-release-announcement/)**! This release brings new features, performance improvements, and important bug fixes. Check out the [release announcement](https://gin-gonic.com/en/blog/news/gin-1-12-0-release-announcement/) on our official blog for the full details. --- @@ -44,7 +44,7 @@ Gin combines the simplicity of Express.js-style routing with Go's performance ch ### Prerequisites -- **Go version**: Gin requires [Go](https://go.dev/) version [1.24](https://go.dev/doc/devel/release#go1.24.0) or above +- **Go version**: Gin requires [Go](https://go.dev/) version [1.25](https://go.dev/doc/devel/release#go1.25.0) or above - **Basic Go knowledge**: Familiarity with Go syntax and package management is helpful ### Installation diff --git a/benchmarks_test.go b/benchmarks_test.go index ca504ecb..5c5163d9 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) { w := newMockWriter() B.ReportAllocs() B.ResetTimer() - for i := 0; i < B.N; i++ { + for B.Loop() { r.ServeHTTP(w, req) } } diff --git a/binding/binding.go b/binding/binding.go index 702d0e82..eced0ae2 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -23,6 +23,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -86,6 +87,7 @@ var ( Header Binding = headerBinding{} Plain BindingBody = plainBinding{} TOML BindingBody = tomlBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -110,6 +112,8 @@ func Default(method, contentType string) Binding { return TOML case MIMEMultipartPOSTForm: return FormMultipart + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go index c8e61310..ae364d79 100644 --- a/binding/binding_nomsgpack.go +++ b/binding/binding_nomsgpack.go @@ -21,6 +21,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -82,6 +83,7 @@ var ( Header = headerBinding{} TOML = tomlBinding{} Plain = plainBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -104,6 +106,8 @@ func Default(method, contentType string) Binding { return FormMultipart case MIMETOML: return TOML + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index 07619ebf..f90488cd 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) + + assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON)) + assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON)) } func TestBindingJSONNilBody(t *testing.T) { @@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) { string(data), string(data[1:])) } +func TestBindingBSON(t *testing.T) { + var obj FooStruct + obj.Foo = "bar" + data, _ := bson.Marshal(&obj) + testBodyBinding(t, + BSON, "bson", + "/", "/", + string(data), + // note: for badbody, we remove first byte to make it invalid + string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) diff --git a/binding/bson.go b/binding/bson.go new file mode 100644 index 00000000..464890f0 --- /dev/null +++ b/binding/bson.go @@ -0,0 +1,30 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "io" + "net/http" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +type bsonBinding struct{} + +func (bsonBinding) Name() string { + return "bson" +} + +func (b bsonBinding) Bind(req *http.Request, obj any) error { + buf, err := io.ReadAll(req.Body) + if err == nil { + err = b.BindBody(buf, obj) + } + return err +} + +func (bsonBinding) BindBody(body []byte, obj any) error { + return bson.Unmarshal(body, obj) +} diff --git a/binding/default_validator.go b/binding/default_validator.go index 44b7a2ac..8203bcaa 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string { } var b strings.Builder - for i := 0; i < len(err); i++ { + for i := range len(err) { if err[i] != nil { if b.Len() > 0 { b.WriteString("\n") @@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error { case reflect.Slice, reflect.Array: count := value.Len() validateRet := make(SliceValidationError, 0) - for i := 0; i < count; i++ { + for i := range count { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { validateRet = append(validateRet, err) } diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e76e7510..6982fd4f 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "errors" "fmt" "maps" @@ -118,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag tValue := value.Type() var isSet bool - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { sf := tValue.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue @@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag type setOptions struct { isDefaultExists bool defaultValue string + // parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`) + parser string } func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { @@ -168,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter setOpt.defaultValue = strings.ReplaceAll(v, ";", ",") } } + } else if k, v = head(opt, "="); k == "parser" { + setOpt.parser = v } } @@ -191,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) { return false, nil } +// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field. +// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this. +func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) { + switch parser { + case "encoding.TextUnmarshaler": + v, ok := value.Addr().Interface().(encoding.TextUnmarshaler) + if !ok { + return false, nil + } + return true, v.UnmarshalText([]byte(val)) + } + return false, nil +} + func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) { cfTag := field.Tag.Get("collection_format") if cfTag == "" || cfTag == "multi" { @@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error case "pipes": sep = "|" default: - return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag) + return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag) } totalLength := 0 @@ -244,7 +263,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, err } - return true, setSlice(vs, value, field) + return true, setSlice(vs, value, field, opt) case reflect.Array: if len(vs) == 0 { if !opt.isDefaultExists { @@ -267,7 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -279,27 +302,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) } - return true, setArray(vs, value, field) + return true, setArray(vs, value, field, opt) default: var val string - if !ok { + if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") { val = opt.defaultValue + } else if len(vs) > 0 { + val = vs[0] } - if len(vs) > 0 { - val = vs[0] - if val == "" { - val = opt.defaultValue - } - } - if ok, err := trySetCustom(val, value); ok { + if ok, err = trySetUsingParser(val, value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(val, value); ok { return ok, err } - return true, setWithProperType(val, value, field) + return true, setWithProperType(val, value, field, opt) } } -func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { +func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error { + // this if-check is required for parsing nested types like []MyId, where MyId is [12]byte + if ok, err := trySetUsingParser(val, value, opt.parser); ok { + return err + } else if ok, err = trySetCustom(val, value); ok { + return err + } + // If it is a string type, no spaces are removed, and the user data is not modified here if value.Kind() != reflect.String { val = strings.TrimSpace(val) @@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel if !value.Elem().IsValid() { value.Set(reflect.New(value.Type().Elem())) } - return setWithProperType(val, value.Elem(), field) + return setWithProperType(val, value.Elem(), field, opt) default: return errUnknownType } @@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val return nil } -func setArray(vals []string, value reflect.Value, field reflect.StructField) error { +func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { for i, s := range vals { - err := setWithProperType(s, value.Index(i), field) + err := setWithProperType(s, value.Index(i), field, opt) if err != nil { return err } @@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err return nil } -func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { +func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) - err := setArray(vals, slice, field) + err := setArray(vals, slice, field, opt) if err != nil { return err } diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index e007573c..c78f7398 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "encoding/hex" "errors" "mime/multipart" @@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) { assert.EqualValues(t, 245, s.Foo) } +func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) { + var s struct { + Foo customUnmarshalParamHex `form:"foo,default=f5"` + } + err := mappingByPtr(&s, formSource{"foo": {}}, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Foo) +} + type customUnmarshalParamType struct { Protocol string Path string @@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) { assert.Equal(t, "foo", s.FileData[1]) } +func TestMappingCustomSliceStopsWhenError(t *testing.T) { + var s struct { + FileData customPath `form:"path"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUri(t *testing.T) { + var s struct { + FileData []customPath `uri:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceForm(t *testing.T) { + var s struct { + FileData []customPath `form:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + type objectID [12]byte func (o *objectID) UnmarshalParam(param string) error { @@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) { assert.Equal(t, expected, s.FileData) } +func TestMappingCustomArrayOfArrayUri(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `uri:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayForm(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `form:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +// ==== TextUnmarshaler tests START ==== + +type customUnmarshalTextHex int + +func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customUnmarshalTextHex(v) + return nil +} + +// verify type implements TextUnmarshaler +var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil) + +func TestMappingCustomUnmarshalTextHexUri(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexForm(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field1": {}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 0xf5, s.Field) +} + +type customUnmarshalTextType struct { + Protocol string + Path string + Name string +} + +func (f *customUnmarshalTextType) UnmarshalText(text []byte) error { + parts := strings.Split(string(text), ":") + if len(parts) != 3 { + return errors.New("invalid format") + } + f.Protocol = parts[0] + f.Path = parts[1] + f.Name = parts[2] + return nil +} + +var _ encoding.TextUnmarshaler = (*customUnmarshalTextType)(nil) + +func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +type customPathUnmarshalText []string + +func (p *customPathUnmarshalText) UnmarshalText(text []byte) error { + elems := strings.Split(string(text), "/") + n := len(elems) + if n < 2 { + return errors.New("invalid format") + } + + *p = elems + return nil +} + +var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil) + +func TestMappingCustomSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +type objectIDUnmarshalText [12]byte + +func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error { + oid, err := convertToOidUnmarshalText(string(text)) + if err != nil { + return err + } + + *o = oid + return nil +} + +func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) { + oid, err := convertTo(s) + return objectIDUnmarshalText(oid), err +} + +var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil) + +func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "uri") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "form") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default +// binding logic. +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalParamHex `form:"hex"` + HexByUnmarshalText customUnmarshalParamHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + "hex2": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) + assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding +} + +// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the +// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards +// compatibility) +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalTextHex `form:"hex"` + HexByUnmarshalText customUnmarshalTextHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`11`}, + "hex2": {`11`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal + assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for normal hex binding +} + +type customHexUnmarshalParamAndUnmarshalText int + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error { + return errors.New("should not be called in unit test if parser tag present") +} + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customHexUnmarshalParamAndUnmarshalText(v) + return nil +} + +// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler, +// then only the UnmarshalText method should be invoked. +func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) { + var s struct { + Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) +} + +// ==== TextUnmarshaler tests END ==== + func TestMappingEmptyValues(t *testing.T) { t.Run("slice with default", func(t *testing.T) { var s struct { diff --git a/context.go b/context.go index c42459ff..5174033e 100644 --- a/context.go +++ b/context.go @@ -40,6 +40,7 @@ const ( MIMEYAML2 = binding.MIMEYAML2 MIMETOML = binding.MIMETOML MIMEPROTOBUF = binding.MIMEPROTOBUF + MIMEBSON = binding.MIMEBSON ) // BodyBytesKey indicates a default body bytes key. @@ -386,6 +387,11 @@ func (c *Context) GetDuration(key any) time.Duration { return getTyped[time.Duration](c, key) } +// GetError returns the value associated with the key as an error. +func (c *Context) GetError(key any) error { + return getTyped[error](c, key) +} + // GetIntSlice returns the value associated with the key as a slice of integers. func (c *Context) GetIntSlice(key any) []int { return getTyped[[]int](c, key) @@ -451,6 +457,11 @@ func (c *Context) GetStringSlice(key any) []string { return getTyped[[]string](c, key) } +// GetErrorSlice returns the value associated with the key as a slice of errors. +func (c *Context) GetErrorSlice(key any) []error { + return getTyped[[]error](c, key) +} + // GetStringMap returns the value associated with the key as a map of interfaces. func (c *Context) GetStringMap(key any) map[string]any { return getTyped[map[string]any](c, key) @@ -740,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) @@ -821,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error { // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. func (c *Context) ShouldBind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) @@ -978,14 +989,27 @@ func (c *Context) ClientIP() string { } } - // It also checks if the remoteIP is a trusted proxy or not. - // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks - // defined by Engine.SetTrustedProxies() - remoteIP := net.ParseIP(c.RemoteIP()) - if remoteIP == nil { - return "" + var ( + trusted bool + remoteIP net.IP + ) + // If gin is listening a unix socket, always trust it. + localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr) + if ok && strings.HasPrefix(localAddr.Network(), "unix") { + trusted = true + } + + // Fallback + if !trusted { + // It also checks if the remoteIP is a trusted proxy or not. + // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks + // defined by Engine.SetTrustedProxies() + remoteIP = net.ParseIP(c.RemoteIP()) + if remoteIP == nil { + return "" + } + trusted = c.engine.isTrustedProxy(remoteIP) } - trusted := c.engine.isTrustedProxy(remoteIP) if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { for _, headerName := range c.engine.RemoteIPHeaders { @@ -1032,9 +1056,10 @@ func (c *Context) requestHeader(key string) string { /************************************/ // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. +// Uses http.StatusContinue constant for better code clarity. func bodyAllowedForStatus(status int) bool { switch { - case status >= 100 && status <= 199: + case status >= http.StatusContinue && status < http.StatusOK: return false case status == http.StatusNoContent: return false @@ -1199,6 +1224,12 @@ func (c *Context) XML(code int, obj any) { c.Render(code, render.XML{Data: obj}) } +// PDF writes the given PDF binary data into the response body. +// It also sets the Content-Type as "application/pdf". +func (c *Context) PDF(code int, data []byte) { + c.Render(code, render.PDF{Data: data}) +} + // YAML serializes the given struct as YAML into the response body. func (c *Context) YAML(code int, obj any) { c.Render(code, render.YAML{Data: obj}) @@ -1214,6 +1245,11 @@ func (c *Context) ProtoBuf(code int, obj any) { c.Render(code, render.ProtoBuf{Data: obj}) } +// BSON serializes the given struct as BSON into the response body. +func (c *Context) BSON(code int, obj any) { + c.Render(code, render.BSON{Data: obj}) +} + // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...any) { c.Render(code, render.String{Format: format, Data: values}) @@ -1321,6 +1357,7 @@ type Negotiate struct { Data any TOMLData any PROTOBUFData any + BSONData any } // Negotiate calls different Render according to acceptable Accept format. @@ -1350,6 +1387,10 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.PROTOBUFData, config.Data) c.ProtoBuf(code, data) + case binding.MIMEBSON: + data := chooseData(config.BSONData, config.Data) + c.BSON(code, data) + default: c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck } diff --git a/context_test.go b/context_test.go index 3080015c..ef60379d 100644 --- a/context_test.go +++ b/context_test.go @@ -32,6 +32,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -516,6 +517,14 @@ func TestContextGetDuration(t *testing.T) { assert.Equal(t, time.Second, c.GetDuration("duration")) } +func TestContextGetError(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + key := "error" + value := errors.New("test error") + c.Set(key, value) + assert.Equal(t, value, c.GetError(key)) +} + func TestContextGetIntSlice(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) key := "int-slice" @@ -618,6 +627,14 @@ func TestContextGetStringSlice(t *testing.T) { assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice")) } +func TestContextGetErrorSlice(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + key := "error-slice" + value := []error{errors.New("error1"), errors.New("error2")} + c.Set(key, value) + assert.Equal(t, value, c.GetErrorSlice(key)) +} + func TestContextGetStringMap(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) m := make(map[string]any) @@ -1014,6 +1031,7 @@ func TestContextGetCookie(t *testing.T) { } func TestContextBodyAllowedForStatus(t *testing.T) { + assert.False(t, bodyAllowedForStatus(http.StatusContinue)) assert.False(t, bodyAllowedForStatus(http.StatusProcessing)) assert.False(t, bodyAllowedForStatus(http.StatusNoContent)) assert.False(t, bodyAllowedForStatus(http.StatusNotModified)) @@ -1302,6 +1320,33 @@ func TestContextRenderNoContentXML(t *testing.T) { assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } +// TestContextRenderPDF tests that the response is serialized as PDF +// and Content-Type is set to application/pdf +func TestContextRenderPDF(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + data := []byte("%Test pdf content") + c.PDF(http.StatusCreated, data) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, data, w.Body.Bytes()) + assert.Equal(t, "application/pdf", w.Header().Get("Content-Type")) +} + +// Tests that no PDF is rendered if code is 204 +func TestContextRenderNoContentPDF(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + data := []byte("%Test pdf content") + c.PDF(http.StatusNoContent, data) + + assert.Equal(t, http.StatusNoContent, w.Code) + assert.Empty(t, w.Body.Bytes()) + assert.Equal(t, "application/pdf", w.Header().Get("Content-Type")) +} + // TestContextRenderString tests that the response is returned // with Content-Type set to text/plain func TestContextRenderString(t *testing.T) { @@ -1685,6 +1730,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) { assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) } +func TestContextNegotiationWithBSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "", nil) + + c.Negotiate(http.StatusOK, Negotiate{ + Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2}, + Data: H{"foo": "bar"}, + }) + + bData, _ := bson.Marshal(H{"foo": "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, string(bData), w.Body.String()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestContextNegotiationNotSupport(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1915,6 +1977,16 @@ func TestContextClientIP(t *testing.T) { c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs() resetContextForClientIPTests(c) + // unix address + addr := &net.UnixAddr{Net: "unix", Name: "@"} + c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr)) + c.Request.RemoteAddr = addr.String() + assert.Equal(t, "20.20.20.20", c.ClientIP()) + + // reset + c.Request = c.Request.WithContext(context.Background()) + resetContextForClientIPTests(c) + // Legacy tests (validating that the defaults don't break the // (insecure!) old behaviour) assert.Equal(t, "20.20.20.20", c.ClientIP()) @@ -2903,6 +2975,16 @@ func TestContextGetRawData(t *testing.T) { assert.Equal(t, "Fetch binary post data", string(data)) } +func TestContextGetRawDataNilBody(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest(http.MethodPost, "/", nil) + + data, err := c.GetRawData() + assert.Nil(t, data) + require.Error(t, err) + assert.Equal(t, "cannot read nil body", err.Error()) +} + func TestContextRenderDataFromReader(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -3491,6 +3573,24 @@ func TestContextSetCookieData(t *testing.T) { setCookie := c.Writer.Header().Get("Set-Cookie") assert.Contains(t, setCookie, "SameSite=None") }) + + // Test that SameSiteDefaultMode inherits from context's sameSite + t.Run("SameSiteDefaultMode inherits context sameSite", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.SetSameSite(http.SameSiteStrictMode) + cookie := &http.Cookie{ + Name: "user", + Value: "gin", + Path: "/", + Domain: "localhost", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteDefaultMode, + } + c.SetCookieData(cookie) + setCookie := c.Writer.Header().Get("Set-Cookie") + assert.Contains(t, setCookie, "SameSite=Strict") + }) } func TestGetMapFromFormData(t *testing.T) { @@ -3651,22 +3751,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) { // Test case 3: Large dataset with many bracket keys largeData := make(map[string][]string) - for i := 0; i < 100; i++ { + for i := range 100 { key := fmt.Sprintf("ids[%d]", i) largeData[key] = []string{fmt.Sprintf("value%d", i)} } - for i := 0; i < 50; i++ { + for i := range 50 { key := fmt.Sprintf("names[%d]", i) largeData[key] = []string{fmt.Sprintf("name%d", i)} } - for i := 0; i < 25; i++ { + for i := range 25 { key := fmt.Sprintf("other[key%d]", i) largeData[key] = []string{fmt.Sprintf("other%d", i)} } // Test case 4: Dataset with many non-matching keys (worst case) worstCaseData := make(map[string][]string) - for i := 0; i < 100; i++ { + for i := range 100 { key := fmt.Sprintf("nonmatching%d", i) worstCaseData[key] = []string{fmt.Sprintf("value%d", i)} } @@ -3702,7 +3802,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) { for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = getMapFromFormData(bm.data, bm.key) } }) diff --git a/debug.go b/debug.go index 1cfa3721..753c1285 100644 --- a/debug.go +++ b/debug.go @@ -13,7 +13,7 @@ import ( "sync/atomic" ) -const ginSupportMinGoVer = 24 +const ginSupportMinGoVer = 25 var runtimeVersion = runtime.Version() @@ -80,7 +80,7 @@ func getMinVer(v string) (uint64, error) { func debugPrintWARNINGDefault() { if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer { - debugPrint(`[WARNING] Now Gin requires Go 1.24+. + debugPrint(`[WARNING] Now Gin requires Go 1.25+. `) } diff --git a/debug_test.go b/debug_test.go index dab02133..bf115ceb 100644 --- a/debug_test.go +++ b/debug_test.go @@ -121,7 +121,7 @@ func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) { debugPrintWARNINGDefault() SetMode(TestMode) }) - assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) + assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.25+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } func TestDebugPrintWARNINGNew(t *testing.T) { diff --git a/docs/doc.md b/docs/doc.md index 0dd86684..7201df5c 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -22,6 +22,7 @@ - [How to write log file](#how-to-write-log-file) - [Custom Log Format](#custom-log-format) - [Controlling Log output coloring](#controlling-log-output-coloring) + - [Avoid logging query strings](#avoid-loging-query-strings) - [Model binding and validation](#model-binding-and-validation) - [Custom Validators](#custom-validators) - [Only Bind Query String](#only-bind-query-string) @@ -592,6 +593,20 @@ func main() { } ``` +### Avoid logging query strings + +```go +func main() { + router := gin.New() + + // SkipQueryString indicates that the logger should not log the query string. + // For example, /path?q=1 will be logged as /path + loggerConfig := gin.LoggerConfig{SkipQueryString: true} + + router.Use(gin.LoggerWithConfig(loggerConfig)) +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz). @@ -911,7 +926,7 @@ curl -X POST http://localhost:8080/person NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply: - Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior -- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values +- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values - Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv" @@ -1009,12 +1024,68 @@ curl -v localhost:8088/thinkerou/not-uuid ### Bind custom unmarshaler +To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound. + ```go package main import ( - "github.com/gin-gonic/gin" + "encoding" "strings" + + "github.com/gin-gonic/gin" +) + +type Birthday string + +func (b *Birthday) UnmarshalText(text []byte) error { + *b = Birthday(strings.Replace(string(text), "-", "/", -1)) + return nil +} + +var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler + +func main() { + route := gin.Default() + var request struct { + Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"` + Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + route.GET("/test", func(ctx *gin.Context) { + _ = ctx.BindQuery(&request) + ctx.JSON(200, request) + }) + _ = route.Run(":8088") +} +``` + +Test it with: + +```sh +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' +``` +Result +```sh +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} +``` + +Note: +- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic. +- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client. + +--- + +If a type already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead. + +```go +package main + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) type Birthday string @@ -1024,29 +1095,37 @@ func (b *Birthday) UnmarshalParam(param string) error { return nil } +var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler + func main() { route := gin.Default() var request struct { - Birthday Birthday `form:"birthday"` + Birthday Birthday `form:"birthday"` + Birthdays []Birthday `form:"birthdays" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"` } route.GET("/test", func(ctx *gin.Context) { _ = ctx.BindQuery(&request) - ctx.JSON(200, request.Birthday) + ctx.JSON(200, request) }) - route.Run(":8088") + _ = route.Run(":8088") } ``` Test it with: ```sh -curl 'localhost:8088/test?birthday=2000-01-01' +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' ``` Result ```sh -"2000/01/01" +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} ``` +Note: +- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag. +- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client. + ### Bind Header ```go diff --git a/errors.go b/errors.go index 829e9d2c..c0d907b9 100644 --- a/errors.go +++ b/errors.go @@ -26,8 +26,6 @@ const ( ErrorTypePublic ErrorType = 1 << 1 // ErrorTypeAny indicates any other error. ErrorTypeAny ErrorType = 1<<64 - 1 - // ErrorTypeNu indicates any other error. - ErrorTypeNu = 2 ) // Error represents a error's specification. diff --git a/gin_integration_test.go b/gin_integration_test.go index 3ea5fe2f..720b140f 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -400,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) { var wg sync.WaitGroup iterations := 200 wg.Add(iterations) - for i := 0; i < iterations; i++ { + for range iterations { go func() { req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(t, err) diff --git a/go.mod b/go.mod index 3a2b2bf2..4fe2ce4f 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,45 @@ module github.com/gin-gonic/gin -go 1.24.0 +go 1.25.0 require ( - github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic v1.15.0 github.com/gin-contrib/sse v1.1.0 - github.com/go-playground/validator/v10 v10.28.0 - github.com/goccy/go-json v0.10.2 - github.com/goccy/go-yaml v1.19.0 + github.com/go-playground/validator/v10 v10.30.1 + github.com/goccy/go-json v0.10.5 + github.com/goccy/go-yaml v1.19.2 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/quic-go/quic-go v0.57.1 + github.com/quic-go/quic-go v0.59.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 - golang.org/x/net v0.47.0 + go.mongodb.org/mongo-driver/v2 v2.5.0 + golang.org/x/net v0.51.0 google.golang.org/protobuf v1.36.10 ) +require gopkg.in/yaml.v3 v3.0.1 // indirect + require ( github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // 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.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index a487aaaf..a75260ce 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -20,12 +20,12 @@ 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -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/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= -github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -41,8 +41,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -51,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -70,21 +71,21 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/bytesconv/bytesconv_test.go b/internal/bytesconv/bytesconv_test.go index 60e28fb4..debfd8c2 100644 --- a/internal/bytesconv/bytesconv_test.go +++ b/internal/bytesconv/bytesconv_test.go @@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte { func TestBytesToString(t *testing.T) { data := make([]byte, 1024) - for i := 0; i < 100; i++ { + for range 100 { _, err := cRand.Read(data) if err != nil { t.Fatal(err) @@ -79,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string { } func TestStringToBytes(t *testing.T) { - for i := 0; i < 100; i++ { + for range 100 { s := RandStringBytesMaskImprSrcSB(64) if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) { t.Fatal("don't match") diff --git a/logger.go b/logger.go index 6441f7ea..cf92553a 100644 --- a/logger.go +++ b/logger.go @@ -48,6 +48,11 @@ type LoggerConfig struct { // Optional. SkipPaths []string + // SkipQueryString indicates that query strings should not be written + // for cases such as when API keys are passed via query strings. + // Optional. Default value is false. + SkipQueryString bool + // Skip is a Skipper that indicates which logs should not be written. // Optional. Skip Skipper @@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.BodySize = c.Writer.Size() - if raw != "" { + if raw != "" && !conf.SkipQueryString { path = path + "?" + raw } diff --git a/logger_test.go b/logger_test.go index 53d0df95..395d97e6 100644 --- a/logger_test.go +++ b/logger_test.go @@ -318,20 +318,21 @@ func TestColorForStatus(t *testing.T) { } func TestColorForLatency(t *testing.T) { - colorForLantency := func(latency time.Duration) string { + colorForLatency := func(latency time.Duration) string { p := LogFormatterParams{ Latency: latency, } return p.LatencyColor() } - assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white") - assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white") - assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green") - assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan") - assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow") - assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta") - assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red") + assert.Equal(t, white, colorForLatency(time.Duration(0)), "0 should be white") + assert.Equal(t, white, colorForLatency(time.Millisecond*20), "20ms should be white") + assert.Equal(t, green, colorForLatency(time.Millisecond*150), "150ms should be green") + assert.Equal(t, cyan, colorForLatency(time.Millisecond*250), "250ms should be cyan") + assert.Equal(t, blue, colorForLatency(time.Millisecond*400), "400ms should be blue") + assert.Equal(t, yellow, colorForLatency(time.Millisecond*600), "600ms should be yellow") + assert.Equal(t, magenta, colorForLatency(time.Millisecond*1500), "1.5s should be magenta") + assert.Equal(t, red, colorForLatency(time.Second*3), "other things should be red") } func TestResetColor(t *testing.T) { @@ -471,3 +472,17 @@ func TestForceConsoleColor(t *testing.T) { // reset console color mode. consoleColorMode = autoColor } + +func TestLoggerWithConfigSkipQueryString(t *testing.T) { + buffer := new(strings.Builder) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + SkipQueryString: true, + })) + router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) }) + + PerformRequest(router, "GET", "/logged?a=21") + assert.Contains(t, buffer.String(), "200") + assert.NotContains(t, buffer.String(), "a=21") +} diff --git a/recovery.go b/recovery.go index 6d4b4b2b..bbf1d565 100644 --- a/recovery.go +++ b/recovery.go @@ -12,12 +12,12 @@ import ( "fmt" "io" "log" - "net" "net/http" "net/http/httputil" "os" "runtime" "strings" + "syscall" "time" "github.com/gin-gonic/gin/internal/bytesconv" @@ -57,40 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { } return func(c *Context) { defer func() { - if err := recover(); err != nil { + if rec := recover(); rec != nil { // Check for a broken connection, as it is not really a // condition that warrants a panic stack trace. - var brokenPipe bool - if ne, ok := err.(*net.OpError); ok { - var se *os.SyscallError - if errors.As(ne, &se) { - seStr := strings.ToLower(se.Error()) - if strings.Contains(seStr, "broken pipe") || - strings.Contains(seStr, "connection reset by peer") { - brokenPipe = true - } - } - } - if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) { - brokenPipe = true + var isBrokenPipe bool + err, ok := rec.(error) + if ok { + isBrokenPipe = errors.Is(err, syscall.EPIPE) || + errors.Is(err, syscall.ECONNRESET) || + errors.Is(err, http.ErrAbortHandler) } if logger != nil { - if brokenPipe { - logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset) + if isBrokenPipe { + logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset) + timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", - timeFormat(time.Now()), err, stack(stackSkip), reset) + timeFormat(time.Now()), rec, stack(stackSkip), reset) } } - if brokenPipe { + if isBrokenPipe { // If the connection is dead, we can't write a status to it. - c.Error(err.(error)) //nolint: errcheck + c.Error(err) //nolint: errcheck c.Abort() } else { - handle(c, err) + handle(c, rec) } } }() diff --git a/recovery_test.go b/recovery_test.go index 912ab601..028c4ad6 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) { router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery", @@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) { router := New() router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) { router.Use(RecoveryWithWriter(nil)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") @@ -98,13 +98,13 @@ func TestFunction(t *testing.T) { func TestPanicWithBrokenPipe(t *testing.T) { const expectCode = 204 - expectMsgs := map[syscall.Errno]string{ - syscall.EPIPE: "broken pipe", - syscall.ECONNRESET: "connection reset by peer", + expectErrnos := []syscall.Errno{ + syscall.EPIPE, + syscall.ECONNRESET, } - for errno, expectMsg := range expectMsgs { - t.Run(expectMsg, func(t *testing.T) { + for _, errno := range expectErrnos { + t.Run("Recovery from "+errno.Error(), func(t *testing.T) { var buf strings.Builder router := New() @@ -122,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) { w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, expectCode, w.Code) - assert.Contains(t, strings.ToLower(buf.String()), expectMsg) + assert.Contains(t, strings.ToLower(buf.String()), errno.Error()) + assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]") }) } } @@ -161,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) { } router.Use(CustomRecoveryWithWriter(buffer, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -180,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -196,14 +197,14 @@ func TestCustomRecovery(t *testing.T) { } router.Use(CustomRecovery(handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -215,7 +216,7 @@ func TestCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -231,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { } router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -250,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } diff --git a/render/bson.go b/render/bson.go new file mode 100644 index 00000000..07f02333 --- /dev/null +++ b/render/bson.go @@ -0,0 +1,34 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "net/http" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// BSON contains the given interface object. +type BSON struct { + Data any +} + +var bsonContentType = []string{"application/bson"} + +// Render (BSON) marshals the given interface object and writes data with custom ContentType. +func (r BSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + + bytes, err := bson.Marshal(&r.Data) + if err == nil { + _, err = w.Write(bytes) + } + return err +} + +// WriteContentType (BSONBuf) writes BSONBuf ContentType. +func (r BSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, bsonContentType) +} diff --git a/render/data.go b/render/data.go index a653ea30..2c0ad5e3 100644 --- a/render/data.go +++ b/render/data.go @@ -4,7 +4,10 @@ package render -import "net/http" +import ( + "net/http" + "strconv" +) // Data contains ContentType and bytes data. type Data struct { @@ -15,6 +18,9 @@ type Data struct { // Render (Data) writes data with custom ContentType. func (r Data) Render(w http.ResponseWriter) (err error) { r.WriteContentType(w) + if len(r.Data) > 0 { + w.Header().Set("Content-Length", strconv.Itoa(len(r.Data))) + } _, err = w.Write(r.Data) return } diff --git a/render/pdf.go b/render/pdf.go new file mode 100644 index 00000000..04dcc1f5 --- /dev/null +++ b/render/pdf.go @@ -0,0 +1,26 @@ +// Copyright 2026 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import "net/http" + +// PDF contains the given PDF binary data. +type PDF struct { + Data []byte +} + +var pdfContentType = []string{"application/pdf"} + +// Render (PDF) writes PDF data with custom ContentType. +func (r PDF) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + _, err := w.Write(r.Data) + return err +} + +// WriteContentType (PDF) writes PDF ContentType for response. +func (r PDF) WriteContentType(w http.ResponseWriter) { + writeContentType(w, pdfContentType) +} diff --git a/render/render.go b/render/render.go index 4bdcfa23..28bc0f5d 100644 --- a/render/render.go +++ b/render/render.go @@ -31,6 +31,7 @@ var ( _ Render = (*AsciiJSON)(nil) _ Render = (*ProtoBuf)(nil) _ Render = (*TOML)(nil) + _ Render = (*PDF)(nil) ) func writeContentType(w http.ResponseWriter, value []string) { diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go index 579897cc..48b23870 100644 --- a/render/render_msgpack_test.go +++ b/render/render_msgpack_test.go @@ -7,7 +7,7 @@ package render import ( - "bytes" + "errors" "net/http/httptest" "testing" @@ -16,9 +16,6 @@ import ( "github.com/ugorji/go/codec" ) -// TODO unit tests -// test errors - func TestRenderMsgPack(t *testing.T) { w := httptest.NewRecorder() data := map[string]any{ @@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) { require.NoError(t, err) - h := new(codec.MsgpackHandle) - assert.NotNil(t, h) - buf := bytes.NewBuffer([]byte{}) - assert.NotNil(t, buf) - err = codec.NewEncoder(buf, h).Encode(data) - + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) require.NoError(t, err) - assert.Equal(t, w.Body.String(), buf.String()) + assert.Equal(t, data, decoded) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) } + +func TestWriteMsgPack(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + "num": 42, + } + + err := WriteMsgPack(w, data) + require.NoError(t, err) + + assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) + + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) + require.NoError(t, err) + assert.Len(t, decoded, 2) + assert.Equal(t, "bar", decoded["foo"]) + assert.EqualValues(t, 42, decoded["num"]) +} + +type failWriter struct { + *httptest.ResponseRecorder +} + +func (w *failWriter) Write(data []byte) (int, error) { + return 0, errors.New("write error") +} + +func TestRenderMsgPackError(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + } + + err := (MsgPack{data}).Render(&failWriter{w}) + require.Error(t, err) + assert.Contains(t, err.Error(), "write error") +} diff --git a/render/render_test.go b/render/render_test.go index d9ae2067..f63878b9 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -8,6 +8,7 @@ import ( "encoding/xml" "errors" "html/template" + "io" "net" "net/http" "net/http/httptest" @@ -15,16 +16,13 @@ import ( "strings" "testing" - "github.com/gin-gonic/gin/codec/json" testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) -// TODO unit tests -// test errors - func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() data := map[string]any{ @@ -139,19 +137,44 @@ func TestRenderJsonpJSON(t *testing.T) { } type errorWriter struct { - bufString string + bufString string + ErrThreshold int // 1-based threshold. If 1, errors on the 1st Write call. + writeCount int *httptest.ResponseRecorder } var _ http.ResponseWriter = (*errorWriter)(nil) +func (w *errorWriter) Header() http.Header { + if w.ResponseRecorder == nil { + w.ResponseRecorder = httptest.NewRecorder() + } + return w.ResponseRecorder.Header() +} + +func (w *errorWriter) WriteHeader(statusCode int) { + if w.ResponseRecorder == nil { + w.ResponseRecorder = httptest.NewRecorder() + } + w.ResponseRecorder.WriteHeader(statusCode) +} + func (w *errorWriter) Write(buf []byte) (int, error) { - if string(buf) == w.bufString { - return 0, errors.New(`write "` + w.bufString + `" error`) + w.writeCount++ + if (w.bufString != "" && string(buf) == w.bufString) || (w.ErrThreshold > 0 && w.writeCount >= w.ErrThreshold) { + return 0, errors.New(`write error`) + } + if w.ResponseRecorder == nil { + w.ResponseRecorder = httptest.NewRecorder() } return w.ResponseRecorder.Write(buf) } +func (w *errorWriter) reset() { + w.writeCount = 0 + w.ResponseRecorder = httptest.NewRecorder() +} + func TestRenderJsonpJSONError(t *testing.T) { ew := &errorWriter{ ResponseRecorder: httptest.NewRecorder(), @@ -164,23 +187,33 @@ func TestRenderJsonpJSONError(t *testing.T) { }, } - cb := template.JSEscapeString(jsonpJSON.Callback) - ew.bufString = cb - err := jsonpJSON.Render(ew) // error was returned while writing callback - assert.Equal(t, `write "`+cb+`" error`, err.Error()) + // error was returned while writing callback + ew.reset() + ew.ErrThreshold = 1 + err := jsonpJSON.Render(ew) + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) - ew.bufString = `(` + // error was returned while writing "(" + ew.reset() + ew.ErrThreshold = 2 err = jsonpJSON.Render(ew) - assert.Equal(t, `write "`+`(`+`" error`, err.Error()) + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) - data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data - ew.bufString = string(data) + // error was returned while writing data + ew.reset() + ew.ErrThreshold = 3 err = jsonpJSON.Render(ew) - assert.Equal(t, `write "`+string(data)+`" error`, err.Error()) + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) - ew.bufString = `);` + // error was returned while writing ");" + ew.reset() + ew.ErrThreshold = 4 err = jsonpJSON.Render(ew) - assert.Equal(t, `write "`+`);`+`" error`, err.Error()) + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) } func TestRenderJsonpJSONError2(t *testing.T) { @@ -359,6 +392,55 @@ func TestRenderProtoBufFail(t *testing.T) { require.Error(t, err) } +func TestRenderBSON(t *testing.T) { + w := httptest.NewRecorder() + reps := []int64{int64(1), int64(2)} + type mystruct struct { + Label string + Reps []int64 + } + + data := &mystruct{ + Label: "test", + Reps: reps, + } + + (BSON{data}).WriteContentType(w) + bsonData, err := bson.Marshal(data) + require.NoError(t, err) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) + + err = (BSON{data}).Render(w) + + require.NoError(t, err) + assert.Equal(t, bsonData, w.Body.Bytes()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + +func TestRenderBSONError(t *testing.T) { + w := httptest.NewRecorder() + data := make(chan int) + + err := (BSON{data}).Render(w) + require.Error(t, err) +} + +func TestRenderBSONWriteError(t *testing.T) { + type testStruct struct { + Value string + } + data := &testStruct{Value: "test"} + + ew := &errorWriter{ + ErrThreshold: 1, + ResponseRecorder: httptest.NewRecorder(), + } + + err := (BSON{data}).Render(ew) + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ @@ -375,6 +457,31 @@ func TestRenderXML(t *testing.T) { assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } +func TestRenderXMLError(t *testing.T) { + w := httptest.NewRecorder() + data := make(chan int) + + err := (XML{data}).Render(w) + require.Error(t, err) + assert.Contains(t, err.Error(), "xml: unsupported type: chan int") +} + +func TestRenderPDF(t *testing.T) { + w := httptest.NewRecorder() + data := []byte("%Test pdf content") + + pdf := PDF{data} + + pdf.WriteContentType(w) + assert.Equal(t, "application/pdf", w.Header().Get("Content-Type")) + + err := pdf.Render(w) + require.NoError(t, err) + + assert.Equal(t, data, w.Body.Bytes()) + assert.Equal(t, "application/pdf", w.Header().Get("Content-Type")) +} + func TestRenderRedirect(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil) require.NoError(t, err) @@ -427,6 +534,52 @@ func TestRenderData(t *testing.T) { require.NoError(t, err) assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "image/png", w.Header().Get("Content-Type")) + assert.Equal(t, "19", w.Header().Get("Content-Length")) +} + +func TestRenderDataContentLength(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + size, err := strconv.Atoi(r.URL.Query().Get("size")) + assert.NoError(t, err) + + data := Data{ + ContentType: "application/octet-stream", + Data: make([]byte, size), + } + assert.NoError(t, data.Render(w)) + })) + t.Cleanup(srv.Close) + + for _, size := range []int{0, 1, 100, 100_000} { + t.Run(strconv.Itoa(size), func(t *testing.T) { + resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type")) + assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length")) + + actual, err := io.Copy(io.Discard, resp.Body) + require.NoError(t, err) + assert.EqualValues(t, size, actual) + }) + } +} + +func TestRenderDataError(t *testing.T) { + ew := &errorWriter{ + ErrThreshold: 1, + ResponseRecorder: httptest.NewRecorder(), + } + data := []byte("#!PNG some raw data") + + err := (Data{ + ContentType: "image/png", + Data: data, + }).Render(ew) + + require.Error(t, err) + assert.Equal(t, "write error", err.Error()) } func TestRenderString(t *testing.T) { @@ -568,6 +721,32 @@ func TestRenderHTMLDebugPanics(t *testing.T) { assert.Panics(t, func() { htmlRender.Instance("", nil) }) } +func TestRenderHTMLTemplateError(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{if .name}}{{.name.DoesNotExist}}{{end}}`)) + + htmlRender := HTMLProduction{Template: templ} + instance := htmlRender.Instance("t", map[string]any{ + "name": "alexandernyquist", + }) + + err := instance.Render(w) + require.Error(t, err) +} + +func TestRenderHTMLTemplateExecuteError(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{.name.invalid}}`)) + + htmlRender := HTMLProduction{Template: templ} + instance := htmlRender.Instance("t", map[string]any{ + "name": "alexandernyquist", + }) + + err := instance.Render(w) + require.Error(t, err) +} + func TestRenderReader(t *testing.T) { w := httptest.NewRecorder() @@ -619,10 +798,10 @@ func TestRenderWriteError(t *testing.T) { prefix := "my-prefix:" r := SecureJSON{Data: data, Prefix: prefix} ew := &errorWriter{ - bufString: prefix, + ErrThreshold: 1, ResponseRecorder: httptest.NewRecorder(), } err := r.Render(ew) require.Error(t, err) - assert.Equal(t, `write "my-prefix:" error`, err.Error()) + assert.Equal(t, "write error", err.Error()) } diff --git a/routergroup.go b/routergroup.go index b2540ec1..c01b917e 100644 --- a/routergroup.go +++ b/routergroup.go @@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { }) } -// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.. +// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead. // router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false}) // Gin by default uses: gin.Dir() func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes { diff --git a/tree.go b/tree.go index 88f25fcb..580abbaf 100644 --- a/tree.go +++ b/tree.go @@ -671,12 +671,7 @@ walk: // Outer loop for walking the tree func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) { const stackBufSize = 128 - // Use a static sized buffer on the stack in the common case. - // If the path is too long, allocate a buffer on the heap instead. - buf := make([]byte, 0, stackBufSize) - if length := len(path) + 1; length > stackBufSize { - buf = make([]byte, 0, length) - } + buf := make([]byte, 0, max(stackBufSize, len(path)+1)) ciPath := n.findCaseInsensitivePathRec( path, @@ -823,7 +818,72 @@ walk: // Outer loop for walking the tree return nil } - n = n.children[0] + // When wildChild is true, try static children first (via indices) + // before falling back to the wildcard child. This ensures that + // case-insensitive lookups prefer static routes over param routes + // (e.g., /PREFIX/XXX should resolve to /prefix/xxx, not match :id). + if len(n.indices) > 0 { + rb = shiftNRuneBytes(rb, npLen) + + if rb[0] != 0 { + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + } else { + var rv rune + var off int + for max_ := min(npLen, 3); off < max_; off++ { + if i := npLen - off; utf8.RuneStart(oldPath[i]) { + rv, _ = utf8.DecodeRuneInString(oldPath[i:]) + break + } + } + + lo := unicode.ToLower(rv) + utf8.EncodeRune(rb[:], lo) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + + if up := unicode.ToUpper(rv); up != lo { + utf8.EncodeRune(rb[:], up) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + } + } + } + + // Fall back to wildcard child, which is always at the end of the array + n = n.children[len(n.children)-1] switch n.nType { case param: // Find param end (either '/' or path end) diff --git a/tree_test.go b/tree_test.go index b580007d..23339af4 100644 --- a/tree_test.go +++ b/tree_test.go @@ -1018,3 +1018,96 @@ func TestWildcardInvalidSlash(t *testing.T) { } } } + +func TestTreeFindCaseInsensitivePathWithMultipleChildrenAndWildcard(t *testing.T) { + tree := &node{} + + // Setup routes that create a node with both static children and a wildcard child. + // This configuration previously caused a panic ("invalid node type") in + // findCaseInsensitivePathRec because it accessed children[0] instead of the + // wildcard child (which is always at the end of the children array). + // See: https://github.com/gin-gonic/gin/issues/2959 + routes := [...]string{ + "/aa/aa", + "/:bb/aa", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // These lookups previously panicked with "invalid node type" because + // findCaseInsensitivePathRec picked children[0] (a static node) instead + // of the wildcard child at the end of the array. + out, found := tree.findCaseInsensitivePath("/aa", true) + if found { + t.Errorf("Expected no match for '/aa', but got: %s", string(out)) + } + + out, found = tree.findCaseInsensitivePath("/aa/aa/aa/aa", true) + if found { + t.Errorf("Expected no match for '/aa/aa/aa/aa', but got: %s", string(out)) + } + + // Case-insensitive lookup should match the static route /aa/aa + out, found = tree.findCaseInsensitivePath("/AA/AA", true) + if !found { + t.Error("Route '/AA/AA' not found via case-insensitive lookup") + } else if string(out) != "/aa/aa" { + t.Errorf("Wrong result for '/AA/AA': expected '/aa/aa', got: %s", string(out)) + } +} + +func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) { + tree := &node{} + + // Another variant: param route + static route under same prefix + routes := [...]string{ + "/prefix/:id", + "/prefix/xxx", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Should NOT panic even for paths that don't match any route + out, found := tree.findCaseInsensitivePath("/prefix/a/b/c", true) + if found { + t.Errorf("Expected no match for '/prefix/a/b/c', but got: %s", string(out)) + } + + // Exact match should still work + out, found = tree.findCaseInsensitivePath("/prefix/xxx", true) + if !found { + t.Error("Route '/prefix/xxx' not found") + } else if string(out) != "/prefix/xxx" { + t.Errorf("Wrong result for '/prefix/xxx': %s", string(out)) + } + + // Case-insensitive match should work + out, found = tree.findCaseInsensitivePath("/PREFIX/XXX", true) + if !found { + t.Error("Route '/PREFIX/XXX' not found via case-insensitive lookup") + } else if string(out) != "/prefix/xxx" { + t.Errorf("Wrong result for '/PREFIX/XXX': expected '/prefix/xxx', got: %s", string(out)) + } + + // Param route should still match + out, found = tree.findCaseInsensitivePath("/prefix/something", true) + if !found { + t.Error("Route '/prefix/something' not found via param match") + } else if string(out) != "/prefix/something" { + t.Errorf("Wrong result for '/prefix/something': %s", string(out)) + } +} diff --git a/utils.go b/utils.go index 62517784..2fecce46 100644 --- a/utils.go +++ b/utils.go @@ -162,7 +162,7 @@ func resolveAddress(addr []string) string { // https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters func isASCII(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] > unicode.MaxASCII { return false } diff --git a/utils_test.go b/utils_test.go index 893ebc88..e1f2c332 100644 --- a/utils_test.go +++ b/utils_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func init() { @@ -145,6 +146,17 @@ func TestMarshalXMLforH(t *testing.T) { assert.Error(t, e) } +func TestMarshalXMLforHSuccess(t *testing.T) { + h := H{ + "key1": "value1", + "key2": 123, + } + data, err := xml.Marshal(h) + require.NoError(t, err) + assert.Contains(t, string(data), "value1") + assert.Contains(t, string(data), "123") +} + func TestIsASCII(t *testing.T) { assert.True(t, isASCII("test")) assert.False(t, isASCII("๐Ÿงก๐Ÿ’›๐Ÿ’š๐Ÿ’™๐Ÿ’œ")) diff --git a/version.go b/version.go index 8049058c..5b54e68a 100644 --- a/version.go +++ b/version.go @@ -5,4 +5,4 @@ package gin // Version is the current gin framework's version. -const Version = "v1.11.0" +const Version = "v1.12.0"