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 @@
[](https://www.codetriage.com/gin-gonic/gin)
[](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"