mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 17:58:14 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
bc415322bc
6
.github/workflows/gin.yml
vendored
6
.github/workflows/gin.yml
vendored
@ -26,14 +26,14 @@ jobs:
|
|||||||
- name: Setup golangci-lint
|
- name: Setup golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.6
|
version: v2.9
|
||||||
args: --verbose
|
args: --verbose
|
||||||
test:
|
test:
|
||||||
needs: lint
|
needs: lint
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
go: ["1.24", "1.25"]
|
go: ["1.25", "1.26"]
|
||||||
test-tags:
|
test-tags:
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ matrix.go-build }}
|
${{ matrix.go-build }}
|
||||||
|
|||||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "^1"
|
go-version: "^1"
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
|
|||||||
32
.github/workflows/trivy-scan.yml
vendored
32
.github/workflows/trivy-scan.yml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
schedule:
|
schedule:
|
||||||
# Run daily at 00:00 UTC
|
# Run daily at 00:00 UTC
|
||||||
- cron: '0 0 * * *'
|
- cron: "0 0 * * *"
|
||||||
workflow_dispatch: # Allow manual trigger
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -27,30 +27,30 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (source code)
|
- name: Run Trivy vulnerability scanner (source code)
|
||||||
uses: aquasecurity/trivy-action@0.33.1
|
uses: aquasecurity/trivy-action@0.34.1
|
||||||
with:
|
with:
|
||||||
scan-type: 'fs'
|
scan-type: "fs"
|
||||||
scan-ref: '.'
|
scan-ref: "."
|
||||||
scanners: 'vuln,secret,misconfig'
|
scanners: "vuln,secret,misconfig"
|
||||||
format: 'sarif'
|
format: "sarif"
|
||||||
output: 'trivy-results.sarif'
|
output: "trivy-results.sarif"
|
||||||
severity: 'CRITICAL,HIGH,MEDIUM'
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
- name: Upload Trivy results to GitHub Security tab
|
- name: Upload Trivy results to GitHub Security tab
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: "trivy-results.sarif"
|
||||||
|
|
||||||
- name: Run Trivy scanner (table output for logs)
|
- name: Run Trivy scanner (table output for logs)
|
||||||
uses: aquasecurity/trivy-action@0.33.1
|
uses: aquasecurity/trivy-action@0.34.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
scan-type: 'fs'
|
scan-type: "fs"
|
||||||
scan-ref: '.'
|
scan-ref: "."
|
||||||
scanners: 'vuln,secret,misconfig'
|
scanners: "vuln,secret,misconfig"
|
||||||
format: 'table'
|
format: "table"
|
||||||
severity: 'CRITICAL,HIGH,MEDIUM'
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
exit-code: '1'
|
exit-code: "1"
|
||||||
|
|||||||
573
CHANGELOG.md
573
CHANGELOG.md
@ -1,74 +1,123 @@
|
|||||||
# Gin ChangeLog
|
# 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
|
## Gin v1.11.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210))
|
- 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(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(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(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(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(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(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633))
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339))
|
- 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(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(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(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(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):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(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(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): 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): 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))
|
- 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))
|
- 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))
|
- context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181))
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987))
|
- 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(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): 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(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): 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): 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(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(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(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077))
|
||||||
|
|
||||||
### Build process updates / CI
|
### Build process updates / CI
|
||||||
|
|
||||||
* ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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): 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(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126))
|
||||||
|
|
||||||
### Dependency updates
|
### 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 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 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 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 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 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 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
|
### Documentation updates
|
||||||
|
|
||||||
* docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360))
|
- 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: 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: 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 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 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: 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(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(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -76,377 +125,377 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* refactor: strengthen HTTPS security and improve code organization
|
- refactor: strengthen HTTPS security and improve code organization
|
||||||
* feat(binding): Support custom BindUnmarshaler for binding. (#3933)
|
- feat(binding): Support custom BindUnmarshaler for binding. (#3933)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940)
|
- 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(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941)
|
||||||
* chore: update external dependencies to latest versions (#3950)
|
- chore: update external dependencies to latest versions (#3950)
|
||||||
* chore: update various Go dependencies to latest versions (#3901)
|
- chore: update various Go dependencies to latest versions (#3901)
|
||||||
* chore: refactor configuration files for better readability (#3951)
|
- chore: refactor configuration files for better readability (#3951)
|
||||||
* chore: update changelog categories and improve documentation (#3917)
|
- chore: update changelog categories and improve documentation (#3917)
|
||||||
* feat: update version constant to v1.10.0 (#3952)
|
- feat: update version constant to v1.10.0 (#3952)
|
||||||
|
|
||||||
### Build process updates
|
### Build process updates
|
||||||
|
|
||||||
* ci(release): refactor changelog regex patterns and exclusions (#3914)
|
- ci(release): refactor changelog regex patterns and exclusions (#3914)
|
||||||
* ci(Makefile): vet command add .PHONY (#3915)
|
- ci(Makefile): vet command add .PHONY (#3915)
|
||||||
|
|
||||||
## Gin v1.10.0
|
## Gin v1.10.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
|
- feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
|
||||||
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
|
- feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
|
||||||
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
|
- feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
|
||||||
* feat(binding): support override default binding implement (#3514) (@ssfyn)
|
- feat(binding): support override default binding implement (#3514) (@ssfyn)
|
||||||
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
|
- feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
|
||||||
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
|
- feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
|
- Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
|
||||||
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
|
- fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
|
||||||
* fix(binding): dereference pointer to struct (#3199) (@echovl)
|
- fix(binding): dereference pointer to struct (#3199) (@echovl)
|
||||||
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
|
- fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
|
||||||
* fix(engine): fix unit test (#3878) (@flc1125)
|
- fix(engine): fix unit test (#3878) (@flc1125)
|
||||||
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
|
- fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
|
||||||
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
|
- fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
|
||||||
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
|
- fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
|
||||||
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
|
- 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(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
|
||||||
* fix(uri): query binding bug (#3236) (@illiafox)
|
- fix(uri): query binding bug (#3236) (@illiafox)
|
||||||
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
- fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
||||||
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
- fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* chore(CI): update release args (#3595) (@qloog)
|
- chore(CI): update release args (#3595) (@qloog)
|
||||||
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
|
- chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
|
||||||
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
|
- chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
|
||||||
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
|
- chore(deps): update dependencies to latest versions (#3835) (@appleboy)
|
||||||
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
|
- chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
|
||||||
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
|
- chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
|
||||||
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
|
- chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
|
||||||
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
|
- chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
|
||||||
* chore(refactor): modify interface check way (#3855) (@demoManito)
|
- chore(refactor): modify interface check way (#3855) (@demoManito)
|
||||||
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
|
- chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
|
||||||
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
|
- chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
|
||||||
* chore: refactor CI and update dependencies (#3848) (@appleboy)
|
- chore: refactor CI and update dependencies (#3848) (@appleboy)
|
||||||
* chore: refactor configuration files for better readability (#3951) (@appleboy)
|
- chore: refactor configuration files for better readability (#3951) (@appleboy)
|
||||||
* chore: update GitHub Actions configuration (#3792) (@appleboy)
|
- chore: update GitHub Actions configuration (#3792) (@appleboy)
|
||||||
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
|
- chore: update changelog categories and improve documentation (#3917) (@appleboy)
|
||||||
* chore: update dependencies to latest versions (#3694) (@appleboy)
|
- chore: update dependencies to latest versions (#3694) (@appleboy)
|
||||||
* chore: update external dependencies to latest versions (#3950) (@appleboy)
|
- chore: update external dependencies to latest versions (#3950) (@appleboy)
|
||||||
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
|
- chore: update various Go dependencies to latest versions (#3901) (@appleboy)
|
||||||
|
|
||||||
### Build process updates
|
### Build process updates
|
||||||
|
|
||||||
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
|
- build(codecov): Added a codecov configuration (#3891) (@flc1125)
|
||||||
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
|
- ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
|
||||||
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
|
- ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
|
||||||
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
|
- ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
|
||||||
* ci(testing): add go1.22 version (#3842) (@appleboy)
|
- ci(testing): add go1.22 version (#3842) (@appleboy)
|
||||||
|
|
||||||
### Documentation updates
|
### Documentation updates
|
||||||
|
|
||||||
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
|
- docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
|
||||||
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
|
- docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
|
||||||
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
|
- docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
|
||||||
* docs: fix typo in comment (#3868) (@testwill)
|
- docs: fix typo in comment (#3868) (@testwill)
|
||||||
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
|
- docs: fix typo in function documentation (#3872) (@TotomiEcio)
|
||||||
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
|
- docs: remove redundant comments (#3765) (@WeiTheShinobi)
|
||||||
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
|
- feat: update version constant to v1.10.0 (#3952) (@appleboy)
|
||||||
|
|
||||||
### Others
|
### Others
|
||||||
|
|
||||||
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
|
- Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
|
||||||
* test(git): gitignore add develop tools (#3370) (@demoManito)
|
- test(git): gitignore add develop tools (#3370) (@demoManito)
|
||||||
* test(http): use constant instead of numeric literal (#3863) (@testwill)
|
- test(http): use constant instead of numeric literal (#3863) (@testwill)
|
||||||
* test(path): Optimize unit test execution results (#3883) (@flc1125)
|
- test(path): Optimize unit test execution results (#3883) (@flc1125)
|
||||||
* test(render): increased unit tests coverage (#3691) (@araujo88)
|
- test(render): increased unit tests coverage (#3691) (@araujo88)
|
||||||
|
|
||||||
## Gin v1.9.1
|
## Gin v1.9.1
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
### 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
|
### ENHANCEMENTS
|
||||||
|
|
||||||
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
|
- 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)
|
- 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)
|
- improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
|
||||||
|
|
||||||
### DOCS
|
### DOCS
|
||||||
|
|
||||||
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
|
- 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)
|
- chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
|
||||||
|
|
||||||
## Gin v1.9.0
|
## Gin v1.9.0
|
||||||
|
|
||||||
### BREAK CHANGES
|
### 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
|
### BUG FIXES
|
||||||
|
|
||||||
* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
|
- 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)
|
- 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)
|
- 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
|
### SECURITY
|
||||||
|
|
||||||
* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
|
- 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(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### ENHANCEMENTS
|
||||||
|
|
||||||
* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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(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(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)
|
- chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
|
||||||
|
|
||||||
### DOCS
|
### DOCS
|
||||||
|
|
||||||
* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
|
- 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): 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(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 [#3446](https://github.com/gin-gonic/gin/pull/3446)
|
||||||
|
|
||||||
## Gin v1.8.2
|
## Gin v1.8.2
|
||||||
|
|
||||||
### BUG FIXES
|
### BUG FIXES
|
||||||
|
|
||||||
* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227)))
|
- 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(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
|
### 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
|
## Gin v1.8.1
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### 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
|
## Gin v1.8.0
|
||||||
|
|
||||||
### BREAK CHANGES
|
### 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`
|
- 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)
|
- gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
|
||||||
|
|
||||||
### BUG FIXES
|
### BUG FIXES
|
||||||
|
|
||||||
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
|
- 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: 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)
|
- Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### ENHANCEMENTS
|
||||||
|
|
||||||
* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
|
- 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)
|
- 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)
|
- 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 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)
|
- 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)
|
- 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 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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 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: 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(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)
|
- 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)
|
- 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)
|
- IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
|
||||||
|
|
||||||
### DOCS
|
### 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
|
## Gin v1.7.7
|
||||||
|
|
||||||
### BUG FIXES
|
### 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).
|
- 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: 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 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).
|
- 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
|
### 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).
|
- 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).
|
- TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
|
||||||
|
|
||||||
### DOCS
|
### 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
|
## Gin v1.7.6
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
## Gin v1.7.4
|
||||||
|
|
||||||
### BUG FIXES
|
### BUG FIXES
|
||||||
|
|
||||||
* bump new release to fix checksum mismatch
|
- bump new release to fix checksum mismatch
|
||||||
|
|
||||||
## Gin v1.7.3
|
## Gin v1.7.3
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
## Gin v1.7.2
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
## Gin v1.7.1
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
## Gin v1.7.0
|
||||||
|
|
||||||
### BUG FIXES
|
### 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 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: 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(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### ENHANCEMENTS
|
||||||
|
|
||||||
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
- 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))
|
- 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))
|
- 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: 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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))
|
- feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
|
||||||
|
|
||||||
## Gin v1.6.3
|
## Gin v1.6.3
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### 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
|
## Gin v1.6.2
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
### 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
|
## Gin v1.6.1
|
||||||
|
|
||||||
### BUG FIXES
|
### 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
|
## Gin v1.6.0
|
||||||
|
|
||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
|
- 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)
|
- 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)
|
- Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
|
||||||
|
|
||||||
### FEATURES
|
### FEATURES
|
||||||
|
|
||||||
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
- add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
||||||
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
|
- FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
|
||||||
|
|
||||||
### BUG FIXES
|
### BUG FIXES
|
||||||
|
|
||||||
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- [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)
|
- Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### ENHANCEMENTS
|
||||||
|
|
||||||
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
|
- 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: 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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: 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 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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 build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
|
||||||
|
|
||||||
### DOCS
|
### DOCS
|
||||||
|
|
||||||
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
|
- 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)
|
- 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)
|
- 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)
|
- 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 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- 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)
|
- Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
|
||||||
|
|
||||||
### MISC
|
### MISC
|
||||||
|
|
||||||
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
|
- 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)
|
- 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)
|
- 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)
|
- fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
|
||||||
|
|
||||||
## Gin v1.5.0
|
## Gin v1.5.0
|
||||||
|
|
||||||
@ -485,14 +534,14 @@
|
|||||||
|
|
||||||
### Gin v1.4.0
|
### 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)
|
- [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] 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)
|
- [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] 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)
|
- [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] 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] 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] 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)
|
- [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] 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)
|
- [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] 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] 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] 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)
|
- [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)
|
- [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)
|
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
|
||||||
|
|
||||||
|
|
||||||
## Gin v1.3.0
|
## 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)
|
- [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] Error implements the json.Marshaller interface
|
||||||
- [FIX] MIT license in every file
|
- [FIX] MIT license in every file
|
||||||
|
|
||||||
|
|
||||||
## Gin 1.0rc1 (May 22, 2015)
|
## Gin 1.0rc1 (May 22, 2015)
|
||||||
|
|
||||||
- [PERFORMANCE] Zero allocation router
|
- [PERFORMANCE] Zero allocation router
|
||||||
@ -681,7 +728,6 @@
|
|||||||
- [FIX] Hijacking http
|
- [FIX] Hijacking http
|
||||||
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
||||||
|
|
||||||
|
|
||||||
## Gin 0.6 (Mar 9, 2015)
|
## Gin 0.6 (Mar 9, 2015)
|
||||||
|
|
||||||
- [NEW] Support multipart/form-data
|
- [NEW] Support multipart/form-data
|
||||||
@ -691,14 +737,12 @@
|
|||||||
- [FIX] Unsigned integers in binding
|
- [FIX] Unsigned integers in binding
|
||||||
- [FIX] Improve color logger
|
- [FIX] Improve color logger
|
||||||
|
|
||||||
|
|
||||||
## Gin 0.5 (Feb 7, 2015)
|
## Gin 0.5 (Feb 7, 2015)
|
||||||
|
|
||||||
- [NEW] Content Negotiation
|
- [NEW] Content Negotiation
|
||||||
- [FIX] Solved security bug that allow a client to spoof ip
|
- [FIX] Solved security bug that allow a client to spoof ip
|
||||||
- [FIX] Fix unexported/ignored fields in binding
|
- [FIX] Fix unexported/ignored fields in binding
|
||||||
|
|
||||||
|
|
||||||
## Gin 0.4 (Aug 21, 2014)
|
## Gin 0.4 (Aug 21, 2014)
|
||||||
|
|
||||||
- [NEW] Development mode
|
- [NEW] Development mode
|
||||||
@ -707,7 +751,6 @@
|
|||||||
- [FIX] Deferring WriteHeader()
|
- [FIX] Deferring WriteHeader()
|
||||||
- [FIX] Improved documentation for model binding
|
- [FIX] Improved documentation for model binding
|
||||||
|
|
||||||
|
|
||||||
## Gin 0.3 (Jul 18, 2014)
|
## Gin 0.3 (Jul 18, 2014)
|
||||||
|
|
||||||
- [PERFORMANCE] Normal log and error log are printed in the same call.
|
- [PERFORMANCE] Normal log and error log are printed in the same call.
|
||||||
@ -725,8 +768,8 @@
|
|||||||
- [FIX] Renaming Context.Req to Context.Request
|
- [FIX] Renaming Context.Req to Context.Request
|
||||||
- [FIX] Check application/x-www-form-urlencoded when parsing form
|
- [FIX] Check application/x-www-form-urlencoded when parsing form
|
||||||
|
|
||||||
|
|
||||||
## Gin 0.2b (Jul 08, 2014)
|
## Gin 0.2b (Jul 08, 2014)
|
||||||
|
|
||||||
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
|
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
|
||||||
- [NEW] Travis CI integration
|
- [NEW] Travis CI integration
|
||||||
- [NEW] Completely new logger
|
- [NEW] Completely new logger
|
||||||
|
|||||||
@ -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).
|
- All tests pass in available continuous integration systems (e.g., GitHub Actions).
|
||||||
- Add or modify tests to cover your code changes.
|
- 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.
|
- 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!
|
Thank you for contributing!
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014 Manuel Martínez-Almeida
|
Copyright (c) 2014-present Manuel Martínez-Almeida
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@ -11,9 +11,9 @@
|
|||||||
[](https://www.codetriage.com/gin-gonic/gin)
|
[](https://www.codetriage.com/gin-gonic/gin)
|
||||||
[](https://github.com/gin-gonic/gin/releases)
|
[](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
|
### 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
|
- **Basic Go knowledge**: Familiarity with Go syntax and package management is helpful
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|||||||
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
|
|||||||
w := newMockWriter()
|
w := newMockWriter()
|
||||||
B.ReportAllocs()
|
B.ReportAllocs()
|
||||||
B.ResetTimer()
|
B.ResetTimer()
|
||||||
for i := 0; i < B.N; i++ {
|
for B.Loop() {
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const (
|
|||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
MIMEYAML2 = "application/yaml"
|
MIMEYAML2 = "application/yaml"
|
||||||
MIMETOML = "application/toml"
|
MIMETOML = "application/toml"
|
||||||
|
MIMEBSON = "application/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -86,6 +87,7 @@ var (
|
|||||||
Header Binding = headerBinding{}
|
Header Binding = headerBinding{}
|
||||||
Plain BindingBody = plainBinding{}
|
Plain BindingBody = plainBinding{}
|
||||||
TOML BindingBody = tomlBinding{}
|
TOML BindingBody = tomlBinding{}
|
||||||
|
BSON BindingBody = bsonBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
|
|||||||
return TOML
|
return TOML
|
||||||
case MIMEMultipartPOSTForm:
|
case MIMEMultipartPOSTForm:
|
||||||
return FormMultipart
|
return FormMultipart
|
||||||
|
case MIMEBSON:
|
||||||
|
return BSON
|
||||||
default: // case MIMEPOSTForm:
|
default: // case MIMEPOSTForm:
|
||||||
return Form
|
return Form
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const (
|
|||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
MIMEYAML2 = "application/yaml"
|
MIMEYAML2 = "application/yaml"
|
||||||
MIMETOML = "application/toml"
|
MIMETOML = "application/toml"
|
||||||
|
MIMEBSON = "application/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -82,6 +83,7 @@ var (
|
|||||||
Header = headerBinding{}
|
Header = headerBinding{}
|
||||||
TOML = tomlBinding{}
|
TOML = tomlBinding{}
|
||||||
Plain = plainBinding{}
|
Plain = plainBinding{}
|
||||||
|
BSON BindingBody = bsonBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
|
|||||||
return FormMultipart
|
return FormMultipart
|
||||||
case MIMETOML:
|
case MIMETOML:
|
||||||
return TOML
|
return TOML
|
||||||
|
case MIMEBSON:
|
||||||
|
return BSON
|
||||||
default: // case MIMEPOSTForm:
|
default: // case MIMEPOSTForm:
|
||||||
return Form
|
return Form
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"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.MethodPost, MIMETOML))
|
||||||
assert.Equal(t, TOML, Default(http.MethodPut, 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) {
|
func TestBindingJSONNilBody(t *testing.T) {
|
||||||
@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
|
|||||||
string(data), string(data[1:]))
|
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) {
|
func TestValidationFails(t *testing.T) {
|
||||||
var obj FooStruct
|
var obj FooStruct
|
||||||
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
||||||
|
|||||||
30
binding/bson.go
Normal file
30
binding/bson.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := 0; i < len(err); i++ {
|
for i := range len(err) {
|
||||||
if err[i] != nil {
|
if err[i] != nil {
|
||||||
if b.Len() > 0 {
|
if b.Len() > 0 {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
|||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
count := value.Len()
|
count := value.Len()
|
||||||
validateRet := make(SliceValidationError, 0)
|
validateRet := make(SliceValidationError, 0)
|
||||||
for i := 0; i < count; i++ {
|
for i := range count {
|
||||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||||
validateRet = append(validateRet, err)
|
validateRet = append(validateRet, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
@ -118,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
tValue := value.Type()
|
tValue := value.Type()
|
||||||
|
|
||||||
var isSet bool
|
var isSet bool
|
||||||
for i := 0; i < value.NumField(); i++ {
|
for i := range value.NumField() {
|
||||||
sf := tValue.Field(i)
|
sf := tValue.Field(i)
|
||||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||||
continue
|
continue
|
||||||
@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
type setOptions struct {
|
type setOptions struct {
|
||||||
isDefaultExists bool
|
isDefaultExists bool
|
||||||
defaultValue string
|
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) {
|
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, ";", ",")
|
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
|
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) {
|
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
||||||
cfTag := field.Tag.Get("collection_format")
|
cfTag := field.Tag.Get("collection_format")
|
||||||
if cfTag == "" || cfTag == "multi" {
|
if cfTag == "" || cfTag == "multi" {
|
||||||
@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
|
|||||||
case "pipes":
|
case "pipes":
|
||||||
sep = "|"
|
sep = "|"
|
||||||
default:
|
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
|
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
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setSlice(vs, value, field)
|
return true, setSlice(vs, value, field, opt)
|
||||||
case reflect.Array:
|
case reflect.Array:
|
||||||
if len(vs) == 0 {
|
if len(vs) == 0 {
|
||||||
if !opt.isDefaultExists {
|
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
|
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 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:
|
default:
|
||||||
var val string
|
var val string
|
||||||
if !ok {
|
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||||
val = opt.defaultValue
|
val = opt.defaultValue
|
||||||
|
} else if len(vs) > 0 {
|
||||||
|
val = vs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(vs) > 0 {
|
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||||
val = vs[0]
|
return ok, err
|
||||||
if val == "" {
|
} else if ok, err = trySetCustom(val, value); ok {
|
||||||
val = opt.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok, err := trySetCustom(val, value); ok {
|
|
||||||
return ok, err
|
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 it is a string type, no spaces are removed, and the user data is not modified here
|
||||||
if value.Kind() != reflect.String {
|
if value.Kind() != reflect.String {
|
||||||
val = strings.TrimSpace(val)
|
val = strings.TrimSpace(val)
|
||||||
@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||||||
if !value.Elem().IsValid() {
|
if !value.Elem().IsValid() {
|
||||||
value.Set(reflect.New(value.Type().Elem()))
|
value.Set(reflect.New(value.Type().Elem()))
|
||||||
}
|
}
|
||||||
return setWithProperType(val, value.Elem(), field)
|
return setWithProperType(val, value.Elem(), field, opt)
|
||||||
default:
|
default:
|
||||||
return errUnknownType
|
return errUnknownType
|
||||||
}
|
}
|
||||||
@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
return nil
|
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 {
|
for i, s := range vals {
|
||||||
err := setWithProperType(s, value.Index(i), field)
|
err := setWithProperType(s, value.Index(i), field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
|
|||||||
return nil
|
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))
|
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||||
err := setArray(vals, slice, field)
|
err := setArray(vals, slice, field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
|||||||
assert.EqualValues(t, 245, s.Foo)
|
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 {
|
type customUnmarshalParamType struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
Path string
|
Path string
|
||||||
@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
|||||||
assert.Equal(t, "foo", s.FileData[1])
|
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
|
type objectID [12]byte
|
||||||
|
|
||||||
func (o *objectID) UnmarshalParam(param string) error {
|
func (o *objectID) UnmarshalParam(param string) error {
|
||||||
@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
|||||||
assert.Equal(t, expected, s.FileData)
|
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) {
|
func TestMappingEmptyValues(t *testing.T) {
|
||||||
t.Run("slice with default", func(t *testing.T) {
|
t.Run("slice with default", func(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
|
|||||||
76
context.go
76
context.go
@ -40,6 +40,7 @@ const (
|
|||||||
MIMEYAML2 = binding.MIMEYAML2
|
MIMEYAML2 = binding.MIMEYAML2
|
||||||
MIMETOML = binding.MIMETOML
|
MIMETOML = binding.MIMETOML
|
||||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||||
|
MIMEBSON = binding.MIMEBSON
|
||||||
)
|
)
|
||||||
|
|
||||||
// BodyBytesKey indicates a default body bytes key.
|
// BodyBytesKey indicates a default body bytes key.
|
||||||
@ -55,14 +56,6 @@ const ContextRequestKey ContextKeyType = 0
|
|||||||
// abortIndex represents a typical value used in abort functions.
|
// abortIndex represents a typical value used in abort functions.
|
||||||
const abortIndex int8 = math.MaxInt8 >> 1
|
const abortIndex int8 = math.MaxInt8 >> 1
|
||||||
|
|
||||||
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
|
|
||||||
func safeInt8(n int) int8 {
|
|
||||||
if n > math.MaxInt8 {
|
|
||||||
return math.MaxInt8
|
|
||||||
}
|
|
||||||
return int8(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
||||||
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
@ -394,6 +387,11 @@ func (c *Context) GetDuration(key any) time.Duration {
|
|||||||
return getTyped[time.Duration](c, key)
|
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.
|
// GetIntSlice returns the value associated with the key as a slice of integers.
|
||||||
func (c *Context) GetIntSlice(key any) []int {
|
func (c *Context) GetIntSlice(key any) []int {
|
||||||
return getTyped[[]int](c, key)
|
return getTyped[[]int](c, key)
|
||||||
@ -459,6 +457,11 @@ func (c *Context) GetStringSlice(key any) []string {
|
|||||||
return getTyped[[]string](c, key)
|
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.
|
// GetStringMap returns the value associated with the key as a map of interfaces.
|
||||||
func (c *Context) GetStringMap(key any) map[string]any {
|
func (c *Context) GetStringMap(key any) map[string]any {
|
||||||
return getTyped[map[string]any](c, key)
|
return getTyped[map[string]any](c, key)
|
||||||
@ -748,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
|
|||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML 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 parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||||
// It decodes the json payload into the struct specified as a pointer.
|
// 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.
|
// 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 {
|
func (c *Context) Bind(obj any) error {
|
||||||
b := binding.Default(c.Request.Method, c.ContentType())
|
b := binding.Default(c.Request.Method, c.ContentType())
|
||||||
@ -829,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
|||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML 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 parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||||
// It decodes the json payload into the struct specified as a pointer.
|
// 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.
|
// 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 {
|
func (c *Context) ShouldBind(obj any) error {
|
||||||
b := binding.Default(c.Request.Method, c.ContentType())
|
b := binding.Default(c.Request.Method, c.ContentType())
|
||||||
@ -986,18 +989,32 @@ func (c *Context) ClientIP() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// It also checks if the remoteIP is a trusted proxy or not.
|
var (
|
||||||
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
trusted bool
|
||||||
// defined by Engine.SetTrustedProxies()
|
remoteIP net.IP
|
||||||
remoteIP := net.ParseIP(c.RemoteIP())
|
)
|
||||||
if remoteIP == nil {
|
// If gin is listening a unix socket, always trust it.
|
||||||
return ""
|
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 {
|
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||||
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
|
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
|
||||||
|
ip, valid := c.engine.validateHeader(headerValue)
|
||||||
if valid {
|
if valid {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
@ -1039,9 +1056,10 @@ func (c *Context) requestHeader(key string) string {
|
|||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
||||||
|
// Uses http.StatusContinue constant for better code clarity.
|
||||||
func bodyAllowedForStatus(status int) bool {
|
func bodyAllowedForStatus(status int) bool {
|
||||||
switch {
|
switch {
|
||||||
case status >= 100 && status <= 199:
|
case status >= http.StatusContinue && status < http.StatusOK:
|
||||||
return false
|
return false
|
||||||
case status == http.StatusNoContent:
|
case status == http.StatusNoContent:
|
||||||
return false
|
return false
|
||||||
@ -1206,6 +1224,12 @@ func (c *Context) XML(code int, obj any) {
|
|||||||
c.Render(code, render.XML{Data: obj})
|
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.
|
// YAML serializes the given struct as YAML into the response body.
|
||||||
func (c *Context) YAML(code int, obj any) {
|
func (c *Context) YAML(code int, obj any) {
|
||||||
c.Render(code, render.YAML{Data: obj})
|
c.Render(code, render.YAML{Data: obj})
|
||||||
@ -1221,6 +1245,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
|
|||||||
c.Render(code, render.ProtoBuf{Data: obj})
|
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.
|
// String writes the given string into the response body.
|
||||||
func (c *Context) String(code int, format string, values ...any) {
|
func (c *Context) String(code int, format string, values ...any) {
|
||||||
c.Render(code, render.String{Format: format, Data: values})
|
c.Render(code, render.String{Format: format, Data: values})
|
||||||
@ -1328,6 +1357,7 @@ type Negotiate struct {
|
|||||||
Data any
|
Data any
|
||||||
TOMLData any
|
TOMLData any
|
||||||
PROTOBUFData any
|
PROTOBUFData any
|
||||||
|
BSONData any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negotiate calls different Render according to acceptable Accept format.
|
// Negotiate calls different Render according to acceptable Accept format.
|
||||||
@ -1357,6 +1387,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
|||||||
data := chooseData(config.PROTOBUFData, config.Data)
|
data := chooseData(config.PROTOBUFData, config.Data)
|
||||||
c.ProtoBuf(code, data)
|
c.ProtoBuf(code, data)
|
||||||
|
|
||||||
|
case binding.MIMEBSON:
|
||||||
|
data := chooseData(config.BSONData, config.Data)
|
||||||
|
c.BSON(code, data)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||||
}
|
}
|
||||||
|
|||||||
145
context_test.go
145
context_test.go
@ -32,6 +32,7 @@ import (
|
|||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -516,6 +517,14 @@ func TestContextGetDuration(t *testing.T) {
|
|||||||
assert.Equal(t, time.Second, c.GetDuration("duration"))
|
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) {
|
func TestContextGetIntSlice(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
key := "int-slice"
|
key := "int-slice"
|
||||||
@ -618,6 +627,14 @@ func TestContextGetStringSlice(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
|
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) {
|
func TestContextGetStringMap(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
m := make(map[string]any)
|
m := make(map[string]any)
|
||||||
@ -1014,6 +1031,7 @@ func TestContextGetCookie(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContextBodyAllowedForStatus(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.StatusProcessing))
|
||||||
assert.False(t, bodyAllowedForStatus(http.StatusNoContent))
|
assert.False(t, bodyAllowedForStatus(http.StatusNoContent))
|
||||||
assert.False(t, bodyAllowedForStatus(http.StatusNotModified))
|
assert.False(t, bodyAllowedForStatus(http.StatusNotModified))
|
||||||
@ -1143,6 +1161,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
// Multiple X-Forwarded-For headers
|
||||||
|
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||||
|
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
|
||||||
|
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||||
|
|
||||||
|
c.engine.ForwardedByClientIP = true
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||||
|
|
||||||
|
// Should return 5.6.7.8 (last non-trusted IP)
|
||||||
|
assert.Equal(t, "5.6.7.8", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextClientIPWithSingleHeader(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||||
|
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||||
|
|
||||||
|
c.engine.ForwardedByClientIP = true
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||||
|
|
||||||
|
// Should return 1.2.3.4
|
||||||
|
assert.Equal(t, "1.2.3.4", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that the response is serialized as Secure JSON
|
// Tests that the response is serialized as Secure JSON
|
||||||
// and Content-Type is set to application/json
|
// and Content-Type is set to application/json
|
||||||
func TestContextRenderSecureJSON(t *testing.T) {
|
func TestContextRenderSecureJSON(t *testing.T) {
|
||||||
@ -1271,6 +1320,33 @@ func TestContextRenderNoContentXML(t *testing.T) {
|
|||||||
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
|
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
|
// TestContextRenderString tests that the response is returned
|
||||||
// with Content-Type set to text/plain
|
// with Content-Type set to text/plain
|
||||||
func TestContextRenderString(t *testing.T) {
|
func TestContextRenderString(t *testing.T) {
|
||||||
@ -1654,6 +1730,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) {
|
|||||||
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
|
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) {
|
func TestContextNegotiationNotSupport(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1884,6 +1977,16 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
|
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
|
||||||
resetContextForClientIPTests(c)
|
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
|
// Legacy tests (validating that the defaults don't break the
|
||||||
// (insecure!) old behaviour)
|
// (insecure!) old behaviour)
|
||||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
@ -1910,7 +2013,7 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
resetContextForClientIPTests(c)
|
resetContextForClientIPTests(c)
|
||||||
|
|
||||||
// IPv6 support
|
// IPv6 support
|
||||||
c.Request.RemoteAddr = "[::1]:12345"
|
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
|
||||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
resetContextForClientIPTests(c)
|
resetContextForClientIPTests(c)
|
||||||
@ -2872,6 +2975,16 @@ func TestContextGetRawData(t *testing.T) {
|
|||||||
assert.Equal(t, "Fetch binary post data", string(data))
|
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) {
|
func TestContextRenderDataFromReader(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -3212,7 +3325,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
addr := strings.Split(l.Addr().String(), ":")
|
addr := strings.Split(l.Addr().String(), ":")
|
||||||
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
|
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(fmt.Errorf("request error: %w", err))
|
t.Error(fmt.Errorf("request error: %w", err))
|
||||||
return
|
return
|
||||||
@ -3460,6 +3573,24 @@ func TestContextSetCookieData(t *testing.T) {
|
|||||||
setCookie := c.Writer.Header().Get("Set-Cookie")
|
setCookie := c.Writer.Header().Get("Set-Cookie")
|
||||||
assert.Contains(t, setCookie, "SameSite=None")
|
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) {
|
func TestGetMapFromFormData(t *testing.T) {
|
||||||
@ -3620,22 +3751,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
|||||||
|
|
||||||
// Test case 3: Large dataset with many bracket keys
|
// Test case 3: Large dataset with many bracket keys
|
||||||
largeData := make(map[string][]string)
|
largeData := make(map[string][]string)
|
||||||
for i := 0; i < 100; i++ {
|
for i := range 100 {
|
||||||
key := fmt.Sprintf("ids[%d]", i)
|
key := fmt.Sprintf("ids[%d]", i)
|
||||||
largeData[key] = []string{fmt.Sprintf("value%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)
|
key := fmt.Sprintf("names[%d]", i)
|
||||||
largeData[key] = []string{fmt.Sprintf("name%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)
|
key := fmt.Sprintf("other[key%d]", i)
|
||||||
largeData[key] = []string{fmt.Sprintf("other%d", i)}
|
largeData[key] = []string{fmt.Sprintf("other%d", i)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test case 4: Dataset with many non-matching keys (worst case)
|
// Test case 4: Dataset with many non-matching keys (worst case)
|
||||||
worstCaseData := make(map[string][]string)
|
worstCaseData := make(map[string][]string)
|
||||||
for i := 0; i < 100; i++ {
|
for i := range 100 {
|
||||||
key := fmt.Sprintf("nonmatching%d", i)
|
key := fmt.Sprintf("nonmatching%d", i)
|
||||||
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
|
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
|
||||||
}
|
}
|
||||||
@ -3671,7 +3802,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
|||||||
for _, bm := range benchmarks {
|
for _, bm := range benchmarks {
|
||||||
b.Run(bm.name, func(b *testing.B) {
|
b.Run(bm.name, func(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
_, _ = getMapFromFormData(bm.data, bm.key)
|
_, _ = getMapFromFormData(bm.data, bm.key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
4
debug.go
4
debug.go
@ -13,7 +13,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 24
|
const ginSupportMinGoVer = 25
|
||||||
|
|
||||||
var runtimeVersion = runtime.Version()
|
var runtimeVersion = runtime.Version()
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ func getMinVer(v string) (uint64, error) {
|
|||||||
|
|
||||||
func debugPrintWARNINGDefault() {
|
func debugPrintWARNINGDefault() {
|
||||||
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
|
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+.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,7 @@ func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
|
|||||||
debugPrintWARNINGDefault()
|
debugPrintWARNINGDefault()
|
||||||
SetMode(TestMode)
|
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) {
|
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||||
|
|||||||
93
docs/doc.md
93
docs/doc.md
@ -22,6 +22,7 @@
|
|||||||
- [How to write log file](#how-to-write-log-file)
|
- [How to write log file](#how-to-write-log-file)
|
||||||
- [Custom Log Format](#custom-log-format)
|
- [Custom Log Format](#custom-log-format)
|
||||||
- [Controlling Log output coloring](#controlling-log-output-coloring)
|
- [Controlling Log output coloring](#controlling-log-output-coloring)
|
||||||
|
- [Avoid logging query strings](#avoid-loging-query-strings)
|
||||||
- [Model binding and validation](#model-binding-and-validation)
|
- [Model binding and validation](#model-binding-and-validation)
|
||||||
- [Custom Validators](#custom-validators)
|
- [Custom Validators](#custom-validators)
|
||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [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
|
### 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).
|
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:
|
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
|
- 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"
|
- 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
|
### 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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"encoding"
|
||||||
"strings"
|
"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
|
type Birthday string
|
||||||
@ -1024,29 +1095,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
route := gin.Default()
|
route := gin.Default()
|
||||||
var request struct {
|
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) {
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
_ = ctx.BindQuery(&request)
|
_ = ctx.BindQuery(&request)
|
||||||
ctx.JSON(200, request.Birthday)
|
ctx.JSON(200, request)
|
||||||
})
|
})
|
||||||
route.Run(":8088")
|
_ = route.Run(":8088")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
|
|
||||||
```sh
|
```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
|
Result
|
||||||
```sh
|
```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
|
### Bind Header
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
@ -26,8 +26,6 @@ const (
|
|||||||
ErrorTypePublic ErrorType = 1 << 1
|
ErrorTypePublic ErrorType = 1 << 1
|
||||||
// ErrorTypeAny indicates any other error.
|
// ErrorTypeAny indicates any other error.
|
||||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||||
// ErrorTypeNu indicates any other error.
|
|
||||||
ErrorTypeNu = 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents a error's specification.
|
// Error represents a error's specification.
|
||||||
|
|||||||
@ -400,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
iterations := 200
|
iterations := 200
|
||||||
wg.Add(iterations)
|
wg.Add(iterations)
|
||||||
for i := 0; i < iterations; i++ {
|
for range iterations {
|
||||||
go func() {
|
go func() {
|
||||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestH2c(t *testing.T) {
|
func TestH2c(t *testing.T) {
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
ln, err := net.Listen("tcp", localhostIP+":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
33
go.mod
33
go.mod
@ -1,42 +1,45 @@
|
|||||||
module github.com/gin-gonic/gin
|
module github.com/gin-gonic/gin
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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/gin-contrib/sse v1.1.0
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/goccy/go-json v0.10.2
|
github.com/goccy/go-json v0.10.5
|
||||||
github.com/goccy/go-yaml v1.19.0
|
github.com/goccy/go-yaml v1.19.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/modern-go/reflect2 v1.0.2
|
github.com/modern-go/reflect2 v1.0.2
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
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/stretchr/testify v1.11.1
|
||||||
github.com/ugorji/go/codec v1.3.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
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
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/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
59
go.sum
59
go.sum
@ -1,17 +1,17 @@
|
|||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
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/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.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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=
|
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/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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-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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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=
|
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/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 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
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=
|
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/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 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte {
|
|||||||
|
|
||||||
func TestBytesToString(t *testing.T) {
|
func TestBytesToString(t *testing.T) {
|
||||||
data := make([]byte, 1024)
|
data := make([]byte, 1024)
|
||||||
for i := 0; i < 100; i++ {
|
for range 100 {
|
||||||
_, err := cRand.Read(data)
|
_, err := cRand.Read(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -79,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStringToBytes(t *testing.T) {
|
func TestStringToBytes(t *testing.T) {
|
||||||
for i := 0; i < 100; i++ {
|
for range 100 {
|
||||||
s := RandStringBytesMaskImprSrcSB(64)
|
s := RandStringBytesMaskImprSrcSB(64)
|
||||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||||
t.Fatal("don't match")
|
t.Fatal("don't match")
|
||||||
|
|||||||
@ -48,6 +48,11 @@ type LoggerConfig struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
SkipPaths []string
|
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.
|
// Skip is a Skipper that indicates which logs should not be written.
|
||||||
// Optional.
|
// Optional.
|
||||||
Skip Skipper
|
Skip Skipper
|
||||||
@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
|
|
||||||
param.BodySize = c.Writer.Size()
|
param.BodySize = c.Writer.Size()
|
||||||
|
|
||||||
if raw != "" {
|
if raw != "" && !conf.SkipQueryString {
|
||||||
path = path + "?" + raw
|
path = path + "?" + raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -318,20 +318,21 @@ func TestColorForStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForLatency(t *testing.T) {
|
func TestColorForLatency(t *testing.T) {
|
||||||
colorForLantency := func(latency time.Duration) string {
|
colorForLatency := func(latency time.Duration) string {
|
||||||
p := LogFormatterParams{
|
p := LogFormatterParams{
|
||||||
Latency: latency,
|
Latency: latency,
|
||||||
}
|
}
|
||||||
return p.LatencyColor()
|
return p.LatencyColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
|
assert.Equal(t, white, colorForLatency(time.Duration(0)), "0 should be white")
|
||||||
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
|
assert.Equal(t, white, colorForLatency(time.Millisecond*20), "20ms should be white")
|
||||||
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
|
assert.Equal(t, green, colorForLatency(time.Millisecond*150), "150ms should be green")
|
||||||
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
|
assert.Equal(t, cyan, colorForLatency(time.Millisecond*250), "250ms should be cyan")
|
||||||
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
|
assert.Equal(t, blue, colorForLatency(time.Millisecond*400), "400ms should be blue")
|
||||||
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
|
assert.Equal(t, yellow, colorForLatency(time.Millisecond*600), "600ms should be yellow")
|
||||||
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
|
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) {
|
func TestResetColor(t *testing.T) {
|
||||||
@ -471,3 +472,17 @@ func TestForceConsoleColor(t *testing.T) {
|
|||||||
// reset console color mode.
|
// reset console color mode.
|
||||||
consoleColorMode = autoColor
|
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")
|
||||||
|
}
|
||||||
|
|||||||
91
recovery.go
91
recovery.go
@ -5,25 +5,28 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dunno = "???"
|
const (
|
||||||
|
dunno = "???"
|
||||||
var dunnoBytes = []byte(dunno)
|
stackSkip = 3
|
||||||
|
)
|
||||||
|
|
||||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||||
type RecoveryFunc func(c *Context, err any)
|
type RecoveryFunc func(c *Context, err any)
|
||||||
@ -54,41 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
|||||||
}
|
}
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if rec := recover(); rec != nil {
|
||||||
// Check for a broken connection, as it is not really a
|
// Check for a broken connection, as it is not really a
|
||||||
// condition that warrants a panic stack trace.
|
// condition that warrants a panic stack trace.
|
||||||
var brokenPipe bool
|
var isBrokenPipe bool
|
||||||
if ne, ok := err.(*net.OpError); ok {
|
err, ok := rec.(error)
|
||||||
var se *os.SyscallError
|
if ok {
|
||||||
if errors.As(ne, &se) {
|
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
|
||||||
seStr := strings.ToLower(se.Error())
|
errors.Is(err, syscall.ECONNRESET) ||
|
||||||
if strings.Contains(seStr, "broken pipe") ||
|
errors.Is(err, http.ErrAbortHandler)
|
||||||
strings.Contains(seStr, "connection reset by peer") {
|
|
||||||
brokenPipe = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
|
|
||||||
brokenPipe = true
|
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
const stackSkip = 3
|
if isBrokenPipe {
|
||||||
if brokenPipe {
|
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
|
||||||
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
|
||||||
} else if IsDebugging() {
|
} else if IsDebugging() {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
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 {
|
} else {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
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.
|
// 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()
|
c.Abort()
|
||||||
} else {
|
} else {
|
||||||
handle(c, err)
|
handle(c, rec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -120,8 +115,11 @@ func stack(skip int) []byte {
|
|||||||
buf := new(bytes.Buffer) // the returned data
|
buf := new(bytes.Buffer) // the returned data
|
||||||
// As we loop, we open files and read them. These variables record the currently
|
// As we loop, we open files and read them. These variables record the currently
|
||||||
// loaded file.
|
// loaded file.
|
||||||
var lines [][]byte
|
var (
|
||||||
var lastFile string
|
nLine string
|
||||||
|
lastFile string
|
||||||
|
err error
|
||||||
|
)
|
||||||
for i := skip; ; i++ { // Skip the expected number of frames
|
for i := skip; ; i++ { // Skip the expected number of frames
|
||||||
pc, file, line, ok := runtime.Caller(i)
|
pc, file, line, ok := runtime.Caller(i)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -130,25 +128,44 @@ func stack(skip int) []byte {
|
|||||||
// Print this much at least. If we can't find the source, it won't show.
|
// Print this much at least. If we can't find the source, it won't show.
|
||||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := os.ReadFile(file)
|
nLine, err = readNthLine(file, line-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
|
||||||
lastFile = file
|
lastFile = file
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
|
||||||
}
|
}
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// source returns a space-trimmed slice of the n'th line.
|
// readNthLine reads the nth line from the file.
|
||||||
func source(lines [][]byte, n int) []byte {
|
// It returns the trimmed content of the line if found,
|
||||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
// or an empty string if the line doesn't exist.
|
||||||
if n < 0 || n >= len(lines) {
|
// If there's an error opening the file, it returns the error.
|
||||||
return dunnoBytes
|
func readNthLine(file string, n int) (string, error) {
|
||||||
|
if n < 0 {
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
return bytes.TrimSpace(lines[n])
|
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner.Scan() {
|
||||||
|
return strings.TrimSpace(scanner.Text()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// function returns, if possible, the name of the function containing the PC.
|
// function returns, if possible, the name of the function containing the PC.
|
||||||
|
|||||||
104
recovery_test.go
104
recovery_test.go
@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
|
|||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
router.GET("/recovery", func(c *Context) {
|
router.GET("/recovery", func(c *Context) {
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery",
|
w := PerformRequest(router, http.MethodGet, "/recovery",
|
||||||
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
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.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
router.Use(RecoveryWithWriter(nil))
|
router.Use(RecoveryWithWriter(nil))
|
||||||
router.GET("/recovery", func(c *Context) {
|
router.GET("/recovery", func(c *Context) {
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSource(t *testing.T) {
|
|
||||||
bs := source(nil, 0)
|
|
||||||
assert.Equal(t, dunnoBytes, bs)
|
|
||||||
|
|
||||||
in := [][]byte{
|
|
||||||
[]byte("Hello world."),
|
|
||||||
[]byte("Hi, gin.."),
|
|
||||||
}
|
|
||||||
bs = source(in, 10)
|
|
||||||
assert.Equal(t, dunnoBytes, bs)
|
|
||||||
|
|
||||||
bs = source(in, 1)
|
|
||||||
assert.Equal(t, []byte("Hello world."), bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFunction(t *testing.T) {
|
func TestFunction(t *testing.T) {
|
||||||
bs := function(1)
|
bs := function(1)
|
||||||
assert.Equal(t, dunno, bs)
|
assert.Equal(t, dunno, bs)
|
||||||
@ -113,13 +98,13 @@ func TestFunction(t *testing.T) {
|
|||||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||||
const expectCode = 204
|
const expectCode = 204
|
||||||
|
|
||||||
expectMsgs := map[syscall.Errno]string{
|
expectErrnos := []syscall.Errno{
|
||||||
syscall.EPIPE: "broken pipe",
|
syscall.EPIPE,
|
||||||
syscall.ECONNRESET: "connection reset by peer",
|
syscall.ECONNRESET,
|
||||||
}
|
}
|
||||||
|
|
||||||
for errno, expectMsg := range expectMsgs {
|
for _, errno := range expectErrnos {
|
||||||
t.Run(expectMsg, func(t *testing.T) {
|
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
|
|
||||||
router := New()
|
router := New()
|
||||||
@ -137,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
|||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, expectCode, w.Code)
|
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]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
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.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -195,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
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)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
@ -211,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(CustomRecovery(handleRecovery))
|
router.Use(CustomRecovery(handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
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.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -230,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
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)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
@ -246,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
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.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -265,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
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)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
@ -331,3 +317,53 @@ func TestSecureRequestDump(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReadNthLine tests the readNthLine function with various scenarios.
|
||||||
|
func TestReadNthLine(t *testing.T) {
|
||||||
|
// Create a temporary test file
|
||||||
|
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
|
||||||
|
tempFile, err := os.CreateTemp("", "testfile*.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
// Write test content to the temporary file
|
||||||
|
if _, err := tempFile.WriteString(testContent); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tempFile.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lineNum int
|
||||||
|
fileName string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
|
||||||
|
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
|
||||||
|
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
|
||||||
|
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||||
|
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||||
|
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := readNthLine(tt.fileName, tt.lineNum)
|
||||||
|
assert.Equal(t, tt.wantErr, err != nil)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStack(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for b.Loop() {
|
||||||
|
_ = stack(stackSkip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
34
render/bson.go
Normal file
34
render/bson.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
// Data contains ContentType and bytes data.
|
// Data contains ContentType and bytes data.
|
||||||
type Data struct {
|
type Data struct {
|
||||||
@ -15,6 +18,9 @@ type Data struct {
|
|||||||
// Render (Data) writes data with custom ContentType.
|
// Render (Data) writes data with custom ContentType.
|
||||||
func (r Data) Render(w http.ResponseWriter) (err error) {
|
func (r Data) Render(w http.ResponseWriter) (err error) {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
|
if len(r.Data) > 0 {
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(r.Data)))
|
||||||
|
}
|
||||||
_, err = w.Write(r.Data)
|
_, err = w.Write(r.Data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
26
render/pdf.go
Normal file
26
render/pdf.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ var (
|
|||||||
_ Render = (*AsciiJSON)(nil)
|
_ Render = (*AsciiJSON)(nil)
|
||||||
_ Render = (*ProtoBuf)(nil)
|
_ Render = (*ProtoBuf)(nil)
|
||||||
_ Render = (*TOML)(nil)
|
_ Render = (*TOML)(nil)
|
||||||
|
_ Render = (*PDF)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeContentType(w http.ResponseWriter, value []string) {
|
func writeContentType(w http.ResponseWriter, value []string) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -16,9 +16,6 @@ import (
|
|||||||
"github.com/ugorji/go/codec"
|
"github.com/ugorji/go/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO unit tests
|
|
||||||
// test errors
|
|
||||||
|
|
||||||
func TestRenderMsgPack(t *testing.T) {
|
func TestRenderMsgPack(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
h := new(codec.MsgpackHandle)
|
var decoded map[string]any
|
||||||
assert.NotNil(t, h)
|
var mh codec.MsgpackHandle
|
||||||
buf := bytes.NewBuffer([]byte{})
|
mh.RawToString = true
|
||||||
assert.NotNil(t, buf)
|
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
|
||||||
err = codec.NewEncoder(buf, h).Encode(data)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
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"))
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -15,16 +16,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/codec/json"
|
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO unit tests
|
|
||||||
// test errors
|
|
||||||
|
|
||||||
func TestRenderJSON(t *testing.T) {
|
func TestRenderJSON(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
@ -139,19 +137,44 @@ func TestRenderJsonpJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type errorWriter struct {
|
type errorWriter struct {
|
||||||
bufString string
|
bufString string
|
||||||
|
ErrThreshold int // 1-based threshold. If 1, errors on the 1st Write call.
|
||||||
|
writeCount int
|
||||||
*httptest.ResponseRecorder
|
*httptest.ResponseRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ http.ResponseWriter = (*errorWriter)(nil)
|
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) {
|
func (w *errorWriter) Write(buf []byte) (int, error) {
|
||||||
if string(buf) == w.bufString {
|
w.writeCount++
|
||||||
return 0, errors.New(`write "` + w.bufString + `" error`)
|
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)
|
return w.ResponseRecorder.Write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *errorWriter) reset() {
|
||||||
|
w.writeCount = 0
|
||||||
|
w.ResponseRecorder = httptest.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderJsonpJSONError(t *testing.T) {
|
func TestRenderJsonpJSONError(t *testing.T) {
|
||||||
ew := &errorWriter{
|
ew := &errorWriter{
|
||||||
ResponseRecorder: httptest.NewRecorder(),
|
ResponseRecorder: httptest.NewRecorder(),
|
||||||
@ -164,23 +187,33 @@ func TestRenderJsonpJSONError(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cb := template.JSEscapeString(jsonpJSON.Callback)
|
// error was returned while writing callback
|
||||||
ew.bufString = cb
|
ew.reset()
|
||||||
err := jsonpJSON.Render(ew) // error was returned while writing callback
|
ew.ErrThreshold = 1
|
||||||
assert.Equal(t, `write "`+cb+`" error`, err.Error())
|
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)
|
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
|
// error was returned while writing data
|
||||||
ew.bufString = string(data)
|
ew.reset()
|
||||||
|
ew.ErrThreshold = 3
|
||||||
err = jsonpJSON.Render(ew)
|
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)
|
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) {
|
func TestRenderJsonpJSONError2(t *testing.T) {
|
||||||
@ -359,6 +392,55 @@ func TestRenderProtoBufFail(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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) {
|
func TestRenderXML(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := xmlmap{
|
data := xmlmap{
|
||||||
@ -375,6 +457,31 @@ func TestRenderXML(t *testing.T) {
|
|||||||
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
|
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) {
|
func TestRenderRedirect(t *testing.T) {
|
||||||
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
|
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -427,6 +534,52 @@ func TestRenderData(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "#!PNG some raw data", w.Body.String())
|
assert.Equal(t, "#!PNG some raw data", w.Body.String())
|
||||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
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) {
|
func TestRenderString(t *testing.T) {
|
||||||
@ -568,6 +721,32 @@ func TestRenderHTMLDebugPanics(t *testing.T) {
|
|||||||
assert.Panics(t, func() { htmlRender.Instance("", nil) })
|
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) {
|
func TestRenderReader(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -619,10 +798,10 @@ func TestRenderWriteError(t *testing.T) {
|
|||||||
prefix := "my-prefix:"
|
prefix := "my-prefix:"
|
||||||
r := SecureJSON{Data: data, Prefix: prefix}
|
r := SecureJSON{Data: data, Prefix: prefix}
|
||||||
ew := &errorWriter{
|
ew := &errorWriter{
|
||||||
bufString: prefix,
|
ErrThreshold: 1,
|
||||||
ResponseRecorder: httptest.NewRecorder(),
|
ResponseRecorder: httptest.NewRecorder(),
|
||||||
}
|
}
|
||||||
err := r.Render(ew)
|
err := r.Render(ew)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Equal(t, `write "my-prefix:" error`, err.Error())
|
assert.Equal(t, "write error", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
|||||||
// Flush implements the http.Flusher interface.
|
// Flush implements the http.Flusher interface.
|
||||||
func (w *responseWriter) Flush() {
|
func (w *responseWriter) Flush() {
|
||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
|||||||
@ -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})
|
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
|
||||||
// Gin by default uses: gin.Dir()
|
// Gin by default uses: gin.Dir()
|
||||||
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
||||||
|
|||||||
83
tree.go
83
tree.go
@ -5,7 +5,6 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@ -78,14 +77,6 @@ func (n *node) addChild(child *node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
|
|
||||||
func safeUint16(n int) uint16 {
|
|
||||||
if n > math.MaxUint16 {
|
|
||||||
return math.MaxUint16
|
|
||||||
}
|
|
||||||
return uint16(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func countParams(path string) uint16 {
|
func countParams(path string) uint16 {
|
||||||
colons := strings.Count(path, ":")
|
colons := strings.Count(path, ":")
|
||||||
stars := strings.Count(path, "*")
|
stars := strings.Count(path, "*")
|
||||||
@ -680,12 +671,7 @@ walk: // Outer loop for walking the tree
|
|||||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||||
const stackBufSize = 128
|
const stackBufSize = 128
|
||||||
|
|
||||||
// Use a static sized buffer on the stack in the common case.
|
buf := make([]byte, 0, max(stackBufSize, len(path)+1))
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ciPath := n.findCaseInsensitivePathRec(
|
ciPath := n.findCaseInsensitivePathRec(
|
||||||
path,
|
path,
|
||||||
@ -832,7 +818,72 @@ walk: // Outer loop for walking the tree
|
|||||||
return nil
|
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 {
|
switch n.nType {
|
||||||
case param:
|
case param:
|
||||||
// Find param end (either '/' or path end)
|
// Find param end (either '/' or path end)
|
||||||
|
|||||||
93
tree_test.go
93
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
25
utils.go
25
utils.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -18,6 +19,12 @@ import (
|
|||||||
// BindKey indicates a default bind key.
|
// BindKey indicates a default bind key.
|
||||||
const BindKey = "_gin-gonic/gin/bindkey"
|
const BindKey = "_gin-gonic/gin/bindkey"
|
||||||
|
|
||||||
|
// localhostIP indicates the default localhost IP address.
|
||||||
|
const localhostIP = "127.0.0.1"
|
||||||
|
|
||||||
|
// localhostIPv6 indicates the default localhost IPv6 address.
|
||||||
|
const localhostIPv6 = "::1"
|
||||||
|
|
||||||
// Bind is a helper function for given interface object and returns a Gin middleware.
|
// Bind is a helper function for given interface object and returns a Gin middleware.
|
||||||
func Bind(val any) HandlerFunc {
|
func Bind(val any) HandlerFunc {
|
||||||
value := reflect.ValueOf(val)
|
value := reflect.ValueOf(val)
|
||||||
@ -155,10 +162,26 @@ func resolveAddress(addr []string) string {
|
|||||||
|
|
||||||
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
||||||
func isASCII(s string) bool {
|
func isASCII(s string) bool {
|
||||||
for i := 0; i < len(s); i++ {
|
for i := range len(s) {
|
||||||
if s[i] > unicode.MaxASCII {
|
if s[i] > unicode.MaxASCII {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
|
||||||
|
func safeInt8(n int) int8 {
|
||||||
|
if n > math.MaxInt8 {
|
||||||
|
return math.MaxInt8
|
||||||
|
}
|
||||||
|
return int8(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
|
||||||
|
func safeUint16(n int) uint16 {
|
||||||
|
if n > math.MaxUint16 {
|
||||||
|
return math.MaxUint16
|
||||||
|
}
|
||||||
|
return uint16(n)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,10 +8,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -144,7 +146,28 @@ func TestMarshalXMLforH(t *testing.T) {
|
|||||||
assert.Error(t, e)
|
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), "<key1>value1</key1>")
|
||||||
|
assert.Contains(t, string(data), "<key2>123</key2>")
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsASCII(t *testing.T) {
|
func TestIsASCII(t *testing.T) {
|
||||||
assert.True(t, isASCII("test"))
|
assert.True(t, isASCII("test"))
|
||||||
assert.False(t, isASCII("🧡💛💚💙💜"))
|
assert.False(t, isASCII("🧡💛💚💙💜"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSafeInt8(t *testing.T) {
|
||||||
|
assert.Equal(t, int8(100), safeInt8(100))
|
||||||
|
assert.Equal(t, int8(math.MaxInt8), safeInt8(int(math.MaxInt8)+123))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeUint16(t *testing.T) {
|
||||||
|
assert.Equal(t, uint16(100), safeUint16(100))
|
||||||
|
assert.Equal(t, uint16(math.MaxUint16), safeUint16(int(math.MaxUint16)+123))
|
||||||
|
}
|
||||||
|
|||||||
@ -5,4 +5,4 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
// Version is the current gin framework's version.
|
// Version is the current gin framework's version.
|
||||||
const Version = "v1.11.0"
|
const Version = "v1.12.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user