Merge branch 'master' into master

This commit is contained in:
Bo-Yi Wu 2026-02-28 21:45:51 +08:00 committed by GitHub
commit baf19c5304
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
96 changed files with 5298 additions and 1201 deletions

View File

@ -1,49 +0,0 @@
- With issues:
- Use the search tool before opening a new issue.
- Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:9000/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:9000/hello/world
<YOUR RESULT>
```
## Environment
- go version:
- gin version (or commit ref):
- operating system:

60
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: ["type/bug"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to appleboy.tw@gmail.com instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions problems on our Discussions Forum (https://github.com/gin-gonic/gin/discussions).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
- type: input
id: gin-ver
attributes:
label: Gin Version
description: Gin version (or commit reference) of your instance
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug?
description: |
If so, please write the steps to reproduce the bug.
options:
- "Yes"
- "No"
validations:
required: true
- type: markdown
attributes:
value: |
It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: textarea
id: source-code
attributes:
label: Source Code
description: If this issue involves source code, please provide a minimal reproducible example
- type: input
id: go-ver
attributes:
label: Go Version
description: The version of Go running on the server
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to run Gin

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Go.dev API Documentation
url: https://pkg.go.dev/github.com/gin-gonic/gin
about: Comprehensive API documentation for Gin.
- name: Gin User Guides
url: https://gin-gonic.com/
about: In-depth user guides and tutorials for using Gin.
- name: Discussions Forum
url: https://github.com/gin-gonic/gin/discussions
about: Questions and configuration or deployment problems can also be discussed.

View File

@ -0,0 +1,18 @@
name: Feature Request
description: Got an idea for a feature that Gin doesn't have currently? Submit your idea here!
labels: ["type/proposal"]
body:
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions problems on our Discussions Forum (https://github.com/gin-gonic/gin/discussions).
3. Please take a moment to check that your feature hasn't already been suggested.
- type: textarea
id: description
attributes:
label: Feature Description
placeholder: |
I think it would be great if Gin had...
validations:
required: true

View File

@ -1,7 +1,10 @@
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
# Pull Request Checklist
Please ensure your pull request meets the following requirements:
- [ ] Open your pull request against the `master` branch.
- [ ] All tests pass in available continuous integration systems (e.g., GitHub Actions).
- [ ] Tests are added or modified as needed to cover code changes.
- [ ] If the pull request introduces a new feature, the feature is documented in the `docs/doc.md`.
Thank you for contributing!

View File

@ -1,10 +1,14 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
interval: daily
- package-ecosystem: github-actions
directory: /
groups:
actions:
patterns:
- "*"
schedule:
interval: daily

View File

@ -33,11 +33,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -46,4 +46,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View File

@ -16,26 +16,32 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "^1"
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: v1.61.0
version: v2.9
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.22", "1.23"]
go: ["1.25", "1.26"]
test-tags:
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
[
"",
"-tags nomsgpack",
'--ldflags="-checklinkname=0" -tags sonic',
"-tags go_json",
"-race",
]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
@ -49,17 +55,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: |
${{ matrix.go-build }}
@ -72,10 +78,6 @@ jobs:
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
- name: Format
if: matrix.go-version == '1.22.x'
run: diff -u <(echo -n) <(gofmt -d .)

View File

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "^1"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
@ -29,3 +29,8 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Go module reindex (pkg.go.dev)
run: |
echo "Triggering Go module reindex at proxy.golang.org"
curl -sSf "https://proxy.golang.org/github.com/${GITHUB_REPOSITORY,,}/@v/${GITHUB_REF_NAME}.info"

56
.github/workflows/trivy-scan.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: "0 0 * * *"
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.34.1
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-results.sarif"
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.34.1
if: always()
with:
scan-type: "fs"
scan-ref: "."
scanners: "vuln,secret,misconfig"
format: "table"
severity: "CRITICAL,HIGH,MEDIUM"
ignore-unfixed: true
exit-code: "1"

View File

@ -1,16 +1,11 @@
run:
timeout: 5m
version: "2"
linters:
enable:
- asciicheck
- copyloopvar
- dogsled
- durationcheck
- errcheck
- errorlint
- exportloopref
- gci
- gofmt
- goimports
- gosec
- misspell
- nakedret
@ -21,52 +16,60 @@ linters:
- testifylint
- usestdlibvars
- wastedassign
linters-settings:
settings:
gosec:
# To select a subset of rules to run.
# Available rules: https://github.com/securego/gosec#available-rules
# Default: [] - means include all rules
includes:
- G102
- G106
- G108
- G109
- G111
- G112
- G201
- G203
excludes:
- G115
perfsprint:
int-conversion: true
err-error: true
errorf: true
fiximports: true
int-conversion: true
sprintf1: true
strconcat: true
testifylint:
enable-all: true
issues:
exclude-rules:
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- structcheck
- unused
text: "`data` is unused"
text: '`data` is unused'
- linters:
- staticcheck
text: "SA1019:"
text: 'SA1019:'
- linters:
- revive
text: "var-naming:"
text: 'var-naming:'
- linters:
- revive
text: "exported:"
- path: _test\.go
linters:
- gosec # security is not make sense in tests
text: 'exported:'
- linters:
- gosec
path: _test\.go
- linters:
- revive
path: _test\.go
- path: gin.go
linters:
- gci
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
settings:
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
exclusions:
generated: lax
paths:
- gin.go

View File

@ -1,358 +1,501 @@
# Gin ChangeLog
## Gin v1.12.0
### Features
- feat(render): add bson protocol ([#4145](https://github.com/gin-gonic/gin/pull/4145))
- feat(context): add GetError and GetErrorSlice methods for error retrieval ([#4502](https://github.com/gin-gonic/gin/pull/4502))
- feat(binding): add support for encoding.UnmarshalText in uri/query binding ([#4203](https://github.com/gin-gonic/gin/pull/4203))
- feat(gin): add option to use escaped path ([#4420](https://github.com/gin-gonic/gin/pull/4420))
- feat(context): add Protocol Buffers support to content negotiation ([#4423](https://github.com/gin-gonic/gin/pull/4423))
- feat(context): implemented Delete method ([#38e7651](https://github.com/gin-gonic/gin/commit/38e7651))
- feat(logger): color latency ([#4146](https://github.com/gin-gonic/gin/pull/4146))
### Enhancements
- perf(tree): reduce allocations in findCaseInsensitivePath ([#4417](https://github.com/gin-gonic/gin/pull/4417))
- perf(recovery): optimize line reading in stack function ([#4466](https://github.com/gin-gonic/gin/pull/4466))
- perf(path): replace regex with custom functions in redirectTrailingSlash ([#4414](https://github.com/gin-gonic/gin/pull/4414))
- perf(tree): optimize path parsing using strings.Count ([#4246](https://github.com/gin-gonic/gin/pull/4246))
- chore(logger): allow skipping query string output ([#4547](https://github.com/gin-gonic/gin/pull/4547))
- chore(context): always trust xff headers from unix socket ([#3359](https://github.com/gin-gonic/gin/pull/3359))
- chore(response): prevent Flush() panic when the underlying ResponseWriter does not implement `http.Flusher` ([#4479](https://github.com/gin-gonic/gin/pull/4479))
- refactor(recovery): smart error comparison ([#4142](https://github.com/gin-gonic/gin/pull/4142))
- refactor(context): replace hardcoded localhost IPs with constants ([#4481](https://github.com/gin-gonic/gin/pull/4481))
- refactor(utils): move util functions to utils.go ([#4467](https://github.com/gin-gonic/gin/pull/4467))
- refactor(binding): use maps.Copy for cleaner map handling ([#4352](https://github.com/gin-gonic/gin/pull/4352))
- refactor(context): using maps.Clone ([#4333](https://github.com/gin-gonic/gin/pull/4333))
- refactor(ginS): use sync.OnceValue to simplify engine function ([#4314](https://github.com/gin-gonic/gin/pull/4314))
- refactor: replace magic numbers with named constants in bodyAllowedForStatus ([#4529](https://github.com/gin-gonic/gin/pull/4529))
- refactor: for loop can be modernized using range over int ([#4392](https://github.com/gin-gonic/gin/pull/4392))
### Bug Fixes
- fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath ([#4535](https://github.com/gin-gonic/gin/pull/4535))
- fix(render): write content length in Data.Render ([#4206](https://github.com/gin-gonic/gin/pull/4206))
- fix(context): ClientIP handling for multiple X-Forwarded-For header values ([#4472](https://github.com/gin-gonic/gin/pull/4472))
- fix(binding): empty value error ([#2169](https://github.com/gin-gonic/gin/pull/2169))
- fix(recover): suppress http.ErrAbortHandler in recover ([#4336](https://github.com/gin-gonic/gin/pull/4336))
- fix(gin): literal colon routes not working with engine.Handler() ([#4415](https://github.com/gin-gonic/gin/pull/4415))
- fix(gin): close os.File in RunFd to prevent resource leak ([#4422](https://github.com/gin-gonic/gin/pull/4422))
- fix(response): refine hijack behavior for response lifecycle ([#4373](https://github.com/gin-gonic/gin/pull/4373))
- fix(binding): improve empty slice/array handling in form binding ([#4380](https://github.com/gin-gonic/gin/pull/4380))
- fix(debug): version mismatch ([#4403](https://github.com/gin-gonic/gin/pull/4403))
- fix: correct typos, improve documentation clarity, and remove dead code ([#4511](https://github.com/gin-gonic/gin/pull/4511))
### Build process updates / CI
- ci: update Go version support to 1.25+ across CI and docs ([#4550](https://github.com/gin-gonic/gin/pull/4550))
- chore(binding): upgrade bson dependency to mongo-driver v2 ([#4549](https://github.com/gin-gonic/gin/pull/4549))
## Gin v1.11.0
### Features
- feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210))
- feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970))
- feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904))
- feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939))
- feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190))
- feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048))
- feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633))
### Enhancements
- perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339))
- refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354))
- refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353))
- refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080))
- refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910))
- refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181))
- refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066))
- refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522))
- refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068))
- refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
- refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
- tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975))
- context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181))
### Bug Fixes
- fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987))
- fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193))
- fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003))
- fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580))
- fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
- fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
- fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413))
- fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222))
- fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077))
### Build process updates / CI
- ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359))
- ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341))
- build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342))
- ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154))
- build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960))
- ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090))
- ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126))
### Dependency updates
- chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356))
- chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347))
- chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351))
- chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328))
- chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221))
- chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052))
### Documentation updates
- docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360))
- docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207))
- docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953))
- docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997))
- docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938))
- docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020))
- docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078))
- docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079))
---
## Gin v1.10.1
### Features
- refactor: strengthen HTTPS security and improve code organization
- feat(binding): Support custom BindUnmarshaler for binding. (#3933)
### Enhancements
- chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940)
- chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941)
- chore: update external dependencies to latest versions (#3950)
- chore: update various Go dependencies to latest versions (#3901)
- chore: refactor configuration files for better readability (#3951)
- chore: update changelog categories and improve documentation (#3917)
- feat: update version constant to v1.10.0 (#3952)
### Build process updates
- ci(release): refactor changelog regex patterns and exclusions (#3914)
- ci(Makefile): vet command add .PHONY (#3915)
## Gin v1.10.0
### Features
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
* feat(binding): support override default binding implement (#3514) (@ssfyn)
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
- feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
- feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
- feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
- feat(binding): support override default binding implement (#3514) (@ssfyn)
- feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
- feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
### Bug fixes
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
* fix(binding): dereference pointer to struct (#3199) (@echovl)
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
* fix(engine): fix unit test (#3878) (@flc1125)
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
* fix(uri): query binding bug (#3236) (@illiafox)
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
- Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
- fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
- fix(binding): dereference pointer to struct (#3199) (@echovl)
- fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
- fix(engine): fix unit test (#3878) (@flc1125)
- fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
- fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
- fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
- fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
- fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
- fix(uri): query binding bug (#3236) (@illiafox)
- fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
- fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
### Enhancements
* chore(CI): update release args (#3595) (@qloog)
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
* chore(refactor): modify interface check way (#3855) (@demoManito)
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
* chore: refactor CI and update dependencies (#3848) (@appleboy)
* chore: refactor configuration files for better readability (#3951) (@appleboy)
* chore: update GitHub Actions configuration (#3792) (@appleboy)
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
* chore: update dependencies to latest versions (#3694) (@appleboy)
* chore: update external dependencies to latest versions (#3950) (@appleboy)
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
- chore(CI): update release args (#3595) (@qloog)
- chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
- chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
- chore(deps): update dependencies to latest versions (#3835) (@appleboy)
- chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
- chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
- chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
- chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
- chore(refactor): modify interface check way (#3855) (@demoManito)
- chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
- chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
- chore: refactor CI and update dependencies (#3848) (@appleboy)
- chore: refactor configuration files for better readability (#3951) (@appleboy)
- chore: update GitHub Actions configuration (#3792) (@appleboy)
- chore: update changelog categories and improve documentation (#3917) (@appleboy)
- chore: update dependencies to latest versions (#3694) (@appleboy)
- chore: update external dependencies to latest versions (#3950) (@appleboy)
- chore: update various Go dependencies to latest versions (#3901) (@appleboy)
### Build process updates
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
* ci(testing): add go1.22 version (#3842) (@appleboy)
- build(codecov): Added a codecov configuration (#3891) (@flc1125)
- ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
- ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
- ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
- ci(testing): add go1.22 version (#3842) (@appleboy)
### Documentation updates
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
* docs: fix typo in comment (#3868) (@testwill)
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
- docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
- docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
- docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
- docs: fix typo in comment (#3868) (@testwill)
- docs: fix typo in function documentation (#3872) (@TotomiEcio)
- docs: remove redundant comments (#3765) (@WeiTheShinobi)
- feat: update version constant to v1.10.0 (#3952) (@appleboy)
### Others
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
* test(git): gitignore add develop tools (#3370) (@demoManito)
* test(http): use constant instead of numeric literal (#3863) (@testwill)
* test(path): Optimize unit test execution results (#3883) (@flc1125)
* test(render): increased unit tests coverage (#3691) (@araujo88)
- Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
- test(git): gitignore add develop tools (#3370) (@demoManito)
- test(http): use constant instead of numeric literal (#3863) (@testwill)
- test(path): Optimize unit test execution results (#3883) (@flc1125)
- test(render): increased unit tests coverage (#3691) (@araujo88)
## Gin v1.9.1
### BUG FIXES
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
- fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
### SECURITY
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
- fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
### ENHANCEMENTS
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
- refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
- convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
- improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
### DOCS
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
- docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
- chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
## Gin v1.9.0
### BREAK CHANGES
* Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
- Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
### BUG FIXES
* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
* fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
* Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
- fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
- fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
- Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
### SECURITY
* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
* fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
- Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
- fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
### ENHANCEMENTS
* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
* chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
* fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
* remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
* refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
* console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
* chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
* chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
* chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
- feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
- chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
- fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
- remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
- refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
- console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
- chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
- chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
- chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
### DOCS
* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
* docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
* docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
* docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
- docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
- docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
- docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
- docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
## Gin v1.8.2
### BUG FIXES
* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227)))
* fix(engine): missing route params for CreateTestContext ([#2778]((https://github.com/gin-gonic/gin/pull/2778))) ([#2803]((https://github.com/gin-gonic/gin/pull/2803)))
- fix(route): redirectSlash bug ([#3227](<(https://github.com/gin-gonic/gin/pull/3227)>))
- fix(engine): missing route params for CreateTestContext ([#2778](<(https://github.com/gin-gonic/gin/pull/2778)>)) ([#2803](<(https://github.com/gin-gonic/gin/pull/2803)>))
### SECURITY
* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432)))
- Fix the GO-2022-1144 vulnerability ([#3432](<(https://github.com/gin-gonic/gin/pull/3432)>))
## Gin v1.8.1
### ENHANCEMENTS
* feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
- feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
## Gin v1.8.0
### BREAK CHANGES
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
* gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
- gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
### BUG FIXES
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
* Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
* Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
- Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
- Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
- Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
### ENHANCEMENTS
* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
* RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
* Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
* Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
* Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
* Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
* Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
* Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
* Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
* Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
* Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
* Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
* Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
* Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
* Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
* Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
* Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
* Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
* Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
* Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
* IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
- Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
- RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
- Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
- Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
- Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
- Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
- Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
- Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
- Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
- Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
- Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
- Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
- Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
- Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
- Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
- Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
- Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
- Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
- Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
- Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
- IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
### DOCS
* Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
- Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
## Gin v1.7.7
### BUG FIXES
* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
- Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
- Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
- Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
- Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
### ENHANCEMENTS
* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
- TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
- TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
### DOCS
* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
- NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
## Gin v1.7.6
### BUG FIXES
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
- bump new release to fix v1.7.5 release error by using v1.7.4 codes.
## Gin v1.7.4
### BUG FIXES
* bump new release to fix checksum mismatch
- bump new release to fix checksum mismatch
## Gin v1.7.3
### BUG FIXES
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
- fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
## Gin v1.7.2
### BUG FIXES
* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
- Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
## Gin v1.7.1
### BUG FIXES
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
- fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
## Gin v1.7.0
### BUG FIXES
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
- fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
- fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
- fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
### ENHANCEMENTS
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
* remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
- Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
- chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
- Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
- chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
- chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
- Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
- update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
- remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
- Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
- Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
- binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
- Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
- Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
- update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
- reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
- implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
- Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
- chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
- Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
- basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
- Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
- feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
## Gin v1.6.3
### ENHANCEMENTS
* Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
- Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
## Gin v1.6.2
### BUG FIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
- fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
- Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1
### BUG FIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
- Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0
### BREAKING
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
- chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
- drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
- Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
- add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
- FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUG FIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
- Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
- Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
- fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
- Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
- [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
- Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
### ENHANCEMENTS
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
- Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
- tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
- tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
- chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
- path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
- Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
- Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
- tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
- tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
- tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
- use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
- upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
- Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
- Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
### DOCS
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
- docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
- Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
- Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
- Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
- Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
- Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
- upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
- Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
- Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
- Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
- docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
- Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
### MISC
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
- ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
- chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
- Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
- fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
## Gin v1.5.0
@ -430,7 +573,7 @@
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
- [FIX] LoadHTML\* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
@ -445,7 +588,6 @@
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
## Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
@ -488,7 +630,7 @@
- [FIX] Refactor render
- [FIX] Reworked tests
- [FIX] logger now supports cygwin
- [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] Use X-Forwarded-For before X-Real-IP
- [FIX] time.Time binding (#904)
## Gin 1.1.4
@ -543,7 +685,6 @@
- [FIX] Error implements the json.Marshaller interface
- [FIX] MIT license in every file
## Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router
@ -587,7 +728,6 @@
- [FIX] Hijacking http
- [FIX] Better support for Google App Engine (using log instead of fmt)
## Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data
@ -597,14 +737,12 @@
- [FIX] Unsigned integers in binding
- [FIX] Improve color logger
## Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
## Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode
@ -613,7 +751,6 @@
- [FIX] Deferring WriteHeader()
- [FIX] Improved documentation for model binding
## Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call.
@ -631,8 +768,8 @@
- [FIX] Renaming Context.Req to Context.Request
- [FIX] Check application/x-www-form-urlencoded when parsing form
## Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration
- [NEW] Completely new logger

View File

@ -1,13 +1,41 @@
## Contributing
# Contributing
- With issues:
- Use the search tool before opening a new issue.
- Please provide source code and commit sha if you found a bug.
We welcome both issue reports and pull requests! Please follow these guidelines to help maintainers respond effectively.
## Issues
- **Before opening a new issue:**
- Use the search tool to check for existing issues or feature requests.
- Review existing issues and provide feedback or react to them.
- Use English for all communications — it is the language all maintainers read and write.
- For questions, configuration or deployment problems, please use the [Discussions Forum](https://github.com/gin-gonic/gin/discussions).
- For bug reports involving sensitive security issues, email <appleboy.tw@gmail.com> instead of posting publicly.
- With pull requests:
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
- **Reporting a bug:**
- Please provide a clear description of your issue, and a minimal reproducible code example if possible.
- Include the Gin version (or commit reference), Go version, and operating system.
- Indicate whether you can reproduce the bug and describe steps to do so.
- Attach relevant logs per [Logging Documentation](https://docs.gitea.com/administration/logging-config#collecting-logs-for-help).
- **Feature requests:**
- Before opening a request, check that a similar idea hasnt already been suggested.
- Clearly describe your proposed feature and its benefits.
_For API Documentation, User Guides, and more, see:_
- [Go.dev API Documentation](https://pkg.go.dev/github.com/gin-gonic/gin)
- [Gin User Guides](https://gin-gonic.com/)
- [Discussions Forum](https://github.com/gin-gonic/gin/discussions)
## Pull Requests
Please ensure your pull request meets the following requirements:
- Open your pull request against the `master` branch.
- Your pull request should have no more than two commits — squash them if necessary.
- All tests pass in available continuous integration systems (e.g., GitHub Actions).
- Add or modify tests to cover your code changes.
- If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README.
- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md).
Thank you for contributing!

189
README.md
View File

@ -2,115 +2,150 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
[![Build Status](https://github.com/gin-gonic/gin/workflows/Run%20Tests/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
[![Build Status](https://github.com/gin-gonic/gin/actions/workflows/gin.yml/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
[![Trivy Security Scan](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter).
If you need performance and good productivity, you will love Gin.
## 📰 Gin 1.12.0 is now available!
**Gin's key features are:**
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.
- Zero allocation router
- Speed
- Middleware support
- Crash-free
- JSON validation
- Route grouping
- Error management
- Built-in rendering
- Extensible
---
## Getting started
Gin is a high-performance HTTP web framework written in [Go](https://go.dev/). It provides a Martini-like API but with significantly better performance—up to 40 times faster—thanks to [httprouter](https://github.com/julienschmidt/httprouter). Gin is designed for building REST APIs, web applications, and microservices where speed and developer productivity are essential.
**Why choose Gin?**
Gin combines the simplicity of Express.js-style routing with Go's performance characteristics, making it ideal for:
- Building high-throughput REST APIs
- Developing microservices that need to handle many concurrent requests
- Creating web applications that require fast response times
- Prototyping web services quickly with minimal boilerplate
**Gin's key features:**
- **Zero allocation router** - Extremely memory-efficient routing with no heap allocations
- **High performance** - Benchmarks show superior speed compared to other Go web frameworks
- **Middleware support** - Extensible middleware system for authentication, logging, CORS, etc.
- **Crash-free** - Built-in recovery middleware prevents panics from crashing your server
- **JSON validation** - Automatic request/response JSON binding and validation
- **Route grouping** - Organize related routes and apply common middleware
- **Error management** - Centralized error handling and logging
- **Built-in rendering** - Support for JSON, XML, HTML templates, and more
- **Extensible** - Large ecosystem of community middleware and plugins
## Getting Started
### Prerequisites
Gin requires [Go](https://go.dev/) version [1.22](https://go.dev/doc/devel/release#go1.22.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
### Getting Gin
### Installation
With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), `go [build|run|test]` automatically fetches the necessary dependencies when you add the import in your code:
With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), simply import Gin in your code and Go will automatically fetch it during build:
```sh
```go
import "github.com/gin-gonic/gin"
```
Alternatively, use `go get`:
### Your First Gin Application
```sh
go get -u github.com/gin-gonic/gin
```
### Running Gin
A basic example:
Here's a complete example that demonstrates Gin's simplicity:
```go
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// Create a Gin router with default middleware (logger and recovery)
r := gin.Default()
// Define a simple GET endpoint
r.GET("/ping", func(c *gin.Context) {
// Return JSON response
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
// Start server on port 8080 (default)
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
if err := r.Run(); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
```
To run the code, use the `go run` command, like:
**Running the application:**
```sh
$ go run example.go
```
1. Save the code above as `main.go`
2. Run the application:
Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response!
```sh
go run main.go
```
### See more examples
3. Open your browser and visit [`http://localhost:8080/ping`](http://localhost:8080/ping)
4. You should see: `{"message":"pong"}`
#### Quick Start
**What this example demonstrates:**
Learn and practice with the [Gin Quick Start](docs/doc.md), which includes API examples and builds tag.
- Creating a Gin router with default middleware
- Defining HTTP endpoints with simple handler functions
- Returning JSON responses
- Starting an HTTP server
#### Examples
### Next Steps
A number of ready-to-run examples demonstrating various use cases of Gin are available in the [Gin examples](https://github.com/gin-gonic/examples) repository.
After running your first Gin application, explore these resources to learn more:
## Documentation
#### 📚 Learning Resources
See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gin).
- **[Gin Quick Start Guide](docs/doc.md)** - Comprehensive tutorial with API examples and build configurations
- **[Example Repository](https://github.com/gin-gonic/examples)** - Ready-to-run examples demonstrating various Gin use cases:
- REST API development
- Authentication & middleware
- File uploads and downloads
- WebSocket connections
- Template rendering
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
## 📖 Documentation
- [English](https://gin-gonic.com/docs/)
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/)
- [Español](https://gin-gonic.com/es/docs/)
- [한국어](https://gin-gonic.com/ko-kr/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/)
- [Persian](https://gin-gonic.com/fa/docs/)
- [Português](https://gin-gonic.com/pt/docs/)
### API Reference
### Articles
- **[Go.dev API Documentation](https://pkg.go.dev/github.com/gin-gonic/gin)** - Complete API reference with examples
- [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
### User Guides
## Benchmarks
The comprehensive documentation is available on [gin-gonic.com](https://gin-gonic.com) in multiple languages:
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md).
- [English](https://gin-gonic.com/en/docs/) | [简体中文](https://gin-gonic.com/zh-cn/docs/) | [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/) | [한국어](https://gin-gonic.com/ko-kr/docs/) | [Español](https://gin-gonic.com/es/docs/)
- [Turkish](https://gin-gonic.com/tr/docs/) | [Persian](https://gin-gonic.com/fa/docs/) | [Português](https://gin-gonic.com/pt/docs/)
- [Russian](https://gin-gonic.com/ru/docs/) | [Indonesian](https://gin-gonic.com/id/docs/)
### Official Tutorials
- [Go.dev Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
## ⚡ Performance Benchmarks
Gin demonstrates exceptional performance compared to other Go web frameworks. It uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) for maximum efficiency. [View detailed benchmarks →](/BENCHMARKS.md)
**Gin vs. Other Go Frameworks** (GitHub API routing benchmark):
| Benchmark name | (1) | (2) | (3) | (4) |
| ------------------------------ | --------: | --------------: | -----------: | --------------: |
@ -150,23 +185,43 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- (3): Heap Memory (B/op), lower is better
- (4): Average Allocations per Repetition (allocs/op), lower is better
## Middleware
## 🔌 Middleware Ecosystem
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
Gin has a rich ecosystem of middleware for common web development needs. Explore community-contributed middleware:
## Uses
- **[gin-contrib](https://github.com/gin-contrib)** - Official middleware collection including:
- Authentication (JWT, Basic Auth, Sessions)
- CORS, Rate limiting, Compression
- Logging, Metrics, Tracing
- Static file serving, Template engines
- **[gin-gonic/contrib](https://github.com/gin-gonic/contrib)** - Additional community middleware
Here are some awesome projects that are using the [Gin](https://github.com/gin-gonic/gin) web framework.
## 🏢 Production Usage
- [gorush](https://github.com/appleboy/gorush): A push notification server.
- [fnproject](https://github.com/fnproject/fn): A container native, cloud agnostic serverless platform.
- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Google TensorFlow.
- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middleware.
- [picfit](https://github.com/thoas/picfit): An image resizing server.
- [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
Gin powers many high-traffic applications and services in production:
## Contributing
- **[gorush](https://github.com/appleboy/gorush)** - High-performance push notification server
- **[fnproject](https://github.com/fnproject/fn)** - Container-native, serverless platform
- **[photoprism](https://github.com/photoprism/photoprism)** - AI-powered personal photo management
- **[lura](https://github.com/luraproject/lura)** - Ultra-performant API Gateway framework
- **[picfit](https://github.com/thoas/picfit)** - Real-time image processing server
- **[dkron](https://github.com/distribworks/dkron)** - Distributed job scheduling system
Gin is the work of hundreds of contributors. We appreciate your help!
## 🤝 Contributing
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
Gin is the work of hundreds of contributors from around the world. We welcome and appreciate your contributions!
### How to Contribute
- 🐛 **Report bugs** - Help us identify and fix issues
- 💡 **Suggest features** - Share your ideas for improvements
- 📝 **Improve documentation** - Help make our docs clearer
- 🔧 **Submit code** - Fix bugs or implement new features
- 🧪 **Write tests** - Improve our test coverage
### Getting Started with Contributing
1. Check out our [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines
2. Join our community discussions and ask questions
**All contributions are valued and help make Gin better for everyone!**

View File

@ -87,7 +87,7 @@ func BenchmarkOneRouteString(B *testing.B) {
runRequest(B, router, http.MethodGet, "/text")
}
func BenchmarkManyRoutesFist(B *testing.B) {
func BenchmarkManyRoutesFirst(B *testing.B) {
router := New()
router.Any("/ping", func(c *Context) {})
runRequest(B, router, http.MethodGet, "/ping")
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
w := newMockWriter()
B.ReportAllocs()
B.ResetTimer()
for i := 0; i < B.N; i++ {
for B.Loop() {
r.ServeHTTP(w, req)
}
}

View File

@ -23,6 +23,7 @@ const (
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
MIMEBSON = "application/bson"
)
// Binding describes the interface which needs to be implemented for binding the
@ -86,6 +87,7 @@ var (
Header Binding = headerBinding{}
Plain BindingBody = plainBinding{}
TOML BindingBody = tomlBinding{}
BSON BindingBody = bsonBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm:
return Form
}

View File

@ -21,6 +21,7 @@ const (
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
MIMEBSON = "application/bson"
)
// Binding describes the interface which needs to be implemented for binding the
@ -82,6 +83,7 @@ var (
Header = headerBinding{}
TOML = tomlBinding{}
Plain = plainBinding{}
BSON BindingBody = bsonBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
return FormMultipart
case MIMETOML:
return TOML
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm:
return Form
}

View File

@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
@ -51,8 +52,6 @@ type FooBarFileStruct struct {
type FooBarFileFailStruct struct {
FooBarStruct
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
// for unexport test
data *multipart.FileHeader `form:"data" binding:"required"`
}
type FooDefaultBarStruct struct {
@ -73,11 +72,15 @@ type FooBarStructForTimeType struct {
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmiCrO"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixMilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"unixMicro"`
}
type FooStructForTimeTypeNotFormat struct {
@ -170,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
}
func TestBindingJSONNilBody(t *testing.T) {
@ -265,10 +271,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
@ -282,11 +288,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, http.MethodGet,
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "/?bar2=foo",
"", "")
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, http.MethodGet,
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
@ -729,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:]))
}
func TestBindingBSON(t *testing.T) {
var obj FooStruct
obj.Foo = "bar"
data, _ := bson.Marshal(&obj)
testBodyBinding(t,
BSON, "bson",
"/", "/",
string(data),
// note: for badbody, we remove first byte to make it invalid
string(data[1:]))
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
@ -952,6 +970,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
assert.Equal(t, int64(1562400033001), obj.UnixMilliTime.UnixMilli())
assert.Equal(t, int64(1562400033000012), obj.UnixMicroTime.UnixMicro())
obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody)
@ -1057,7 +1077,7 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
}
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "", obj.TestName)
assert.Empty(t, obj.TestName)
obj = InvalidNameType{}
req = requestWithBody(method, badPath, badBody)
@ -1312,7 +1332,7 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
req := requestWithBody(http.MethodPost, path, body)
err := b.Bind(req, &obj)
require.Error(t, err)
assert.Equal(t, "", obj.Foo)
assert.Empty(t, obj.Foo)
obj = FooStruct{}
req = requestWithBody(http.MethodPost, badPath, badBody)

30
binding/bson.go Normal file
View 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)
}

View File

@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
}
var b strings.Builder
for i := 0; i < len(err); i++ {
for i := range len(err) {
if err[i] != nil {
if b.Len() > 0 {
b.WriteString("\n")
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
case reflect.Slice, reflect.Array:
count := value.Len()
validateRet := make(SliceValidationError, 0)
for i := 0; i < count; i++ {
for i := range count {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err)
}

View File

@ -18,9 +18,8 @@ func BenchmarkSliceValidationError(b *testing.B) {
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
if len(e.Error()) == 0 {
b.Errorf("error")
}

View File

@ -18,14 +18,16 @@ func TestSliceValidationError(t *testing.T) {
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
{"has zero elements", SliceValidationError{}, ""},
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements",
{
"has two elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),
},
"[0]: first error\n[1]: second error",
},
{"has many elements",
{
"has many elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),

View File

@ -11,9 +11,11 @@ import (
const defaultMemory = 32 << 20
type formBinding struct{}
type formPostBinding struct{}
type formMultipartBinding struct{}
type (
formBinding struct{}
formPostBinding struct{}
formMultipartBinding struct{}
)
func (formBinding) Name() string {
return "form"

View File

@ -5,16 +5,18 @@
package binding
import (
"encoding"
"errors"
"fmt"
"maps"
"mime/multipart"
"reflect"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
)
var (
@ -117,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
tValue := value.Type()
var isSet bool
for i := 0; i < value.NumField(); i++ {
for i := range value.NumField() {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
@ -136,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
type setOptions struct {
isDefaultExists bool
defaultValue string
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
parser string
}
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
@ -167,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} else if k, v = head(opt, "="); k == "parser" {
setOpt.parser = v
}
}
@ -175,7 +181,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
// UnmarshalParam decodes and assigns a value from a form or query param.
UnmarshalParam(param string) error
}
@ -190,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil
}
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
switch parser {
case "encoding.TextUnmarshaler":
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
if !ok {
return false, nil
}
return true, v.UnmarshalText([]byte(val))
}
return false, nil
}
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -207,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
}
totalLength := 0
@ -230,9 +250,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() {
case reflect.Slice:
if !ok {
vs = []string{opt.defaultValue}
if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -240,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
}
@ -248,11 +273,14 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, err
}
return true, setSlice(vs, value, field)
return true, setSlice(vs, value, field, opt)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -260,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
}
@ -272,27 +302,37 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
return true, setArray(vs, value, field, opt)
default:
var val string
if !ok {
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
val = opt.defaultValue
} else if len(vs) > 0 {
val = vs[0]
}
if len(vs) > 0 {
val = vs[0]
if val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field)
return true, setWithProperType(val, value, field, opt)
}
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
return err
} else if ok, err = trySetCustom(val, value); ok {
return err
}
// If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String {
val = strings.TrimSpace(val)
}
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
@ -333,14 +373,14 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case multipart.FileHeader:
return nil
}
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
return setWithProperType(val, value.Elem(), field, opt)
default:
return errUnknownType
}
@ -397,28 +437,34 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
var t time.Time
switch tf {
case "unix":
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC
@ -441,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil
}
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
for i, s := range vals {
err := setWithProperType(s, value.Index(i), field)
err := setWithProperType(s, value.Index(i), field, opt)
if err != nil {
return err
}
@ -451,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
return nil
}
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field)
err := setArray(vals, slice, field, opt)
if err != nil {
return err
}
@ -462,6 +508,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
}
func setTimeDuration(val string, value reflect.Value) error {
if val == "" {
val = "0"
}
d, err := time.ParseDuration(val)
if err != nil {
return err
@ -483,9 +533,7 @@ func setFormMap(ptr any, form map[string][]string) error {
if !ok {
return ErrConvertMapStringSlice
}
for k, v := range form {
ptrMap[k] = v
}
maps.Copy(ptrMap, form)
return nil
}

View File

@ -31,7 +31,7 @@ type structFull struct {
func BenchmarkMapFormFull(b *testing.B) {
var s structFull
for i := 0; i < b.N; i++ {
for b.Loop() {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
@ -54,7 +54,7 @@ type structName struct {
func BenchmarkMapFormName(b *testing.B) {
var s structName
for i := 0; i < b.N; i++ {
for b.Loop() {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")

View File

@ -5,6 +5,7 @@
package binding
import (
"encoding"
"encoding/hex"
"errors"
"mime/multipart"
@ -226,7 +227,35 @@ func TestMappingTime(t *testing.T) {
require.Error(t, err)
}
type bindTestData struct {
need any
got any
in map[string][]string
}
func TestMappingTimeUnixNano(t *testing.T) {
type needFixUnixNanoEmpty struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
}
// ok
tests := []bindTestData{
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
}
func TestMappingTimeDuration(t *testing.T) {
type needFixDurationEmpty struct {
Duration time.Duration `form:"duration"`
}
var s struct {
D time.Duration
}
@ -236,6 +265,17 @@ func TestMappingTimeDuration(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// ok
tests := []bindTestData{
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err)
@ -485,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
assert.EqualValues(t, 245, s.Foo)
}
func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo,default=f5"`
}
err := mappingByPtr(&s, formSource{"foo": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Foo)
}
type customUnmarshalParamType struct {
Protocol string
Path string
@ -509,9 +559,9 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
@ -521,9 +571,9 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
@ -533,9 +583,9 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
@ -545,9 +595,9 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
type customPath []string
@ -570,8 +620,8 @@ func TestMappingCustomSliceUri(t *testing.T) {
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceForm(t *testing.T) {
@ -581,8 +631,35 @@ func TestMappingCustomSliceForm(t *testing.T) {
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
assert.Equal(t, "bar", s.FileData[0])
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
@ -621,7 +698,7 @@ func TestMappingCustomArrayUri(t *testing.T) {
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
@ -633,5 +710,437 @@ func TestMappingCustomArrayForm(t *testing.T) {
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(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) {
t.Run("slice with default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present but empty
err = mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present with values
err = mappingByPtr(&s, formSource{"slice": {"1", "2", "3"}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.Slice)
})
t.Run("array with default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
// field present but empty
err = mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
})
t.Run("slice without default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int(nil), s.Slice)
})
t.Run("array without default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{0}, s.Array)
})
t.Run("slice with collection format", func(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi,default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:"slice_csv,default=1;2;3" collection_format:"csv"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
// field present but empty
err = mappingByPtr(&s, formSource{"slice_multi": {}, "slice_csv": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
})
}

View File

@ -17,7 +17,6 @@ func (headerBinding) Name() string {
}
func (headerBinding) Bind(req *http.Request, obj any) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}

View File

@ -10,7 +10,7 @@ import (
"io"
"net/http"
"github.com/gin-gonic/gin/internal/json"
"github.com/gin-gonic/gin/codec/json"
)
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
}
func decodeJSON(r io.Reader, obj any) error {
decoder := json.NewDecoder(r)
decoder := json.API.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}

View File

@ -5,8 +5,16 @@
package binding
import (
"io"
"net/http/httptest"
"testing"
"time"
"unsafe"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/render"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}
func TestCustomJsonCodec(t *testing.T) {
// Restore json encoding configuration after testing
oldMarshal := json.API
defer func() {
json.API = oldMarshal
}()
// Custom json api
json.API = customJsonApi{}
// test decode json
obj := customReq{}
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
require.NoError(t, err)
assert.Equal(t, zeroTime, obj.TimeEmpty)
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
assert.Nil(t, obj.TimeNil)
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
// test encode json
w := httptest.NewRecorder()
err2 := (render.PureJSON{Data: obj}).Render(w)
require.NoError(t, err2)
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type customReq struct {
TimeEmpty time.Time `json:"time_empty"`
TimeStruct time.Time `json:"time_struct"`
TimeNil *time.Time `json:"time_nil"`
TimePointer *time.Time `json:"time_pointer"`
}
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
func init() {
customConfig.RegisterExtension(&TimeEx{})
customConfig.RegisterExtension(&TimePointerEx{})
}
type customJsonApi struct{}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
// region Time Extension
var (
zeroTime = time.Time{}
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
defaultTimeCodec = &timeCodec{}
)
type TimeEx struct {
jsoniter.DummyExtension
}
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
type timeCodec struct{}
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((*time.Time)(ptr))
return t.Equal(zeroTime)
}
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((*time.Time)(ptr))
if t.Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((*time.Time)(ptr)) = zeroTime
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((*time.Time)(ptr)) = t
}
// endregion
// region *Time Extension
var (
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
defaultTimePointerCodec = &timePointerCodec{}
)
type TimePointerEx struct {
jsoniter.DummyExtension
}
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
type timePointerCodec struct{}
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((**time.Time)(ptr))
return t == nil || (*t).Equal(zeroTime)
}
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((**time.Time)(ptr))
if t == nil || (*t).Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((**time.Time)(ptr)) = nil
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((**time.Time)(ptr)) = &t
}
// endregion

View File

@ -15,7 +15,7 @@ func (plainBinding) Name() string {
return "plain"
}
func (plainBinding) Bind(req *http.Request, obj interface{}) error {
func (plainBinding) Bind(req *http.Request, obj any) error {
all, err := io.ReadAll(req.Body)
if err != nil {
return err

View File

@ -34,7 +34,7 @@ func (protobufBinding) BindBody(body []byte, obj any) error {
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add
// Here it's same to return validate(obj), but until now we can't add
// `binding:""` to the struct which automatically generate by gen-proto
return nil
// return validate(obj)

View File

@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
return validate(obj)
}

View File

@ -158,16 +158,16 @@ type structNoValidationPointer struct {
}
func TestValidateNoValidationPointers(t *testing.T) {
//origin := createNoValidation_values()
//test := createNoValidation_values()
// origin := createNoValidation_values()
// test := createNoValidation_values()
empty := structNoValidationPointer{}
//assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test))
// assert.Nil(t, validate(test))
// assert.Nil(t, validate(&test))
require.NoError(t, validate(empty))
require.NoError(t, validate(&empty))
//assert.Equal(t, origin, test)
// assert.Equal(t, origin, test)
}
type Object map[string]any
@ -198,7 +198,7 @@ type structModifyValidation struct {
}
func toZero(sl validator.StructLevel) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
s := sl.Top().Interface().(*structModifyValidation)
s.Integer = 0
}
@ -249,5 +249,5 @@ func TestValidatorEngine(t *testing.T) {
// Check that we got back non-nil errs
require.Error(t, errs)
// Check that the error matches expectation
require.Error(t, errs, "", "", "notone")
require.Error(t, errs, "notone")
}

View File

@ -24,6 +24,7 @@ func (xmlBinding) Bind(req *http.Request, obj any) error {
func (xmlBinding) BindBody(body []byte, obj any) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj any) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {

View File

@ -9,7 +9,7 @@ import (
"io"
"net/http"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
)
type yamlBinding struct{}

57
codec/json/api.go Normal file
View File

@ -0,0 +1,57 @@
// 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 json
import "io"
// API the json codec in use.
var API Core
// Core the api for json codec.
type Core interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
MarshalIndent(v any, prefix, indent string) ([]byte, error)
NewEncoder(writer io.Writer) Encoder
NewDecoder(reader io.Reader) Decoder
}
// Encoder an interface writes JSON values to an output stream.
type Encoder interface {
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
SetEscapeHTML(on bool)
// Encode writes the JSON encoding of v to the stream,
// followed by a newline character.
//
// See the documentation for Marshal for details about the
// conversion of Go values to JSON.
Encode(v any) error
}
// Decoder an interface reads and decodes JSON values from an input stream.
type Decoder interface {
// UseNumber causes the Decoder to unmarshal a number into an any as a
// Number instead of as a float64.
UseNumber()
// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
DisallowUnknownFields()
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
Decode(v any) error
}

42
codec/json/go_json.go Normal file
View File

@ -0,0 +1,42 @@
// 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.
//go:build go_json
package json
import (
"io"
"github.com/goccy/go-json"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/goccy/go-json"
func init() {
API = gojsonApi{}
}
type gojsonApi struct{}
func (j gojsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j gojsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

41
codec/json/json.go Normal file
View File

@ -0,0 +1,41 @@
// 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.
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
package json
import (
"encoding/json"
"io"
)
// Package indicates what library is being used for JSON encoding.
const Package = "encoding/json"
func init() {
API = jsonApi{}
}
type jsonApi struct{}
func (j jsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

44
codec/json/jsoniter.go Normal file
View File

@ -0,0 +1,44 @@
// 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.
//go:build jsoniter
package json
import (
"io"
jsoniter "github.com/json-iterator/go"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/json-iterator/go"
func init() {
API = jsoniterApi{}
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type jsoniterApi struct{}
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

44
codec/json/sonic.go Normal file
View File

@ -0,0 +1,44 @@
// 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.
//go:build sonic && (linux || windows || darwin)
package json
import (
"io"
"github.com/bytedance/sonic"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/bytedance/sonic"
func init() {
API = sonicApi{}
}
var json = sonic.ConfigStd
type sonicApi struct{}
func (j sonicApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j sonicApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -6,9 +6,11 @@ package gin
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"maps"
"math"
"mime/multipart"
"net"
@ -37,6 +39,8 @@ const (
MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML
MIMEPROTOBUF = binding.MIMEPROTOBUF
MIMEBSON = binding.MIMEBSON
)
// BodyBytesKey indicates a default body bytes key.
@ -72,7 +76,7 @@ type Context struct {
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]any
Keys map[any]any
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
@ -129,11 +133,8 @@ func (c *Context) Copy() *Context {
cp.fullPath = c.fullPath
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v
}
cp.Keys = maps.Clone(cKeys)
c.mu.RUnlock()
cParams := c.Params
@ -186,7 +187,7 @@ func (c *Context) FullPath() string {
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
for c.index < safeInt8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
@ -215,6 +216,14 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort()
}
// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
// This method stops the chain, writes the status code and return a JSON body without escaping.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
c.Abort()
c.PureJSON(code, jsonObj)
}
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json".
@ -264,11 +273,11 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
func (c *Context) Set(key any, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
c.Keys = make(map[any]any)
}
c.Keys[key] = value
@ -276,7 +285,7 @@ func (c *Context) Set(key string, value any) {
// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
func (c *Context) Get(key any) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
@ -284,14 +293,14 @@ func (c *Context) Get(key string) (value any, exists bool) {
}
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) any {
func (c *Context) MustGet(key any) any {
if value, exists := c.Get(key); exists {
return value
}
panic("Key \"" + key + "\" does not exist")
panic(fmt.Sprintf("key %v does not exist", key))
}
func getTyped[T any](c *Context, key string) (res T) {
func getTyped[T any](c *Context, key any) (res T) {
if val, ok := c.Get(key); ok && val != nil {
res, _ = val.(T)
}
@ -299,165 +308,185 @@ func getTyped[T any](c *Context, key string) (res T) {
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
func (c *Context) GetString(key any) string {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) {
func (c *Context) GetBool(key any) bool {
return getTyped[bool](c, key)
}
// GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) {
func (c *Context) GetInt(key any) int {
return getTyped[int](c, key)
}
// GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt8(key string) (i8 int8) {
func (c *Context) GetInt8(key any) int8 {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key string) (i16 int16) {
func (c *Context) GetInt16(key any) int16 {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key string) (i32 int32) {
func (c *Context) GetInt32(key any) int32 {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key string) (i64 int64) {
func (c *Context) GetInt64(key any) int64 {
return getTyped[int64](c, key)
}
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) {
func (c *Context) GetUint(key any) uint {
return getTyped[uint](c, key)
}
// GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint8(key string) (ui8 uint8) {
func (c *Context) GetUint8(key any) uint8 {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key string) (ui16 uint16) {
func (c *Context) GetUint16(key any) uint16 {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key string) (ui32 uint32) {
func (c *Context) GetUint32(key any) uint32 {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key string) (ui64 uint64) {
func (c *Context) GetUint64(key any) uint64 {
return getTyped[uint64](c, key)
}
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key string) (f32 float32) {
func (c *Context) GetFloat32(key any) float32 {
return getTyped[float32](c, key)
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
func (c *Context) GetFloat64(key any) float64 {
return getTyped[float64](c, key)
}
// GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) {
func (c *Context) GetTime(key any) time.Time {
return getTyped[time.Time](c, key)
}
// GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) {
func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key)
}
// GetError returns the value associated with the key as an error.
func (c *Context) GetError(key any) error {
return getTyped[error](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key string) (is []int) {
func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key string) (i8s []int8) {
func (c *Context) GetInt8Slice(key any) []int8 {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key string) (i16s []int16) {
func (c *Context) GetInt16Slice(key any) []int16 {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key string) (i32s []int32) {
func (c *Context) GetInt32Slice(key any) []int32 {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key string) (i64s []int64) {
func (c *Context) GetInt64Slice(key any) []int64 {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key string) (uis []uint) {
func (c *Context) GetUintSlice(key any) []uint {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key string) (ui8s []uint8) {
func (c *Context) GetUint8Slice(key any) []uint8 {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key string) (ui16s []uint16) {
func (c *Context) GetUint16Slice(key any) []uint16 {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key string) (ui32s []uint32) {
func (c *Context) GetUint32Slice(key any) []uint32 {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key string) (ui64s []uint64) {
func (c *Context) GetUint64Slice(key any) []uint64 {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key string) (f32s []float32) {
func (c *Context) GetFloat32Slice(key any) []float32 {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key string) (f64s []float64) {
func (c *Context) GetFloat64Slice(key any) []float64 {
return getTyped[[]float64](c, key)
}
// GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) {
func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key)
}
// GetErrorSlice returns the value associated with the key as a slice of errors.
func (c *Context) GetErrorSlice(key any) []error {
return getTyped[[]error](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]any) {
func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key)
}
// GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) {
func (c *Context) GetStringMapString(key any) map[string]string {
return getTyped[map[string]string](c, key)
}
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
return getTyped[map[string][]string](c, key)
}
// Delete deletes the key from the Context's Key map, if it exists.
// This operation is safe to be used by concurrent go-routines
func (c *Context) Delete(key any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys != nil {
delete(c.Keys, key)
}
}
/************************************/
/************ INPUT DATA ************/
/************************************/
@ -564,7 +593,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.initQueryCache()
return c.get(c.queryCache, key)
return getMapFromFormData(c.queryCache, key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
@ -637,22 +666,32 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.initFormCache()
return c.get(c.formCache, key)
return getMapFromFormData(c.formCache, key)
}
// get is an internal method and returns a map which satisfies conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
// getMapFromFormData return a map which satisfies conditions.
// It parses from data with bracket notation like "key[subkey]=value" into a map.
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
d := make(map[string]string)
found := false
keyLen := len(key)
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
continue
}
if k[:keyLen] != key || k[keyLen] != '[' {
continue
}
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
found = true
d[k[keyLen+1:keyLen+1+j]] = v[0]
}
}
}
return dicts, exist
return d, found
}
// FormFile returns the first file for the provided form key.
@ -684,15 +723,15 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
}
defer src.Close()
if len(perm) <= 0 {
perm = append(perm, 0o750)
var mode os.FileMode = 0o750
if len(perm) > 0 {
mode = perm[0]
}
if err = os.MkdirAll(filepath.Dir(dst), perm[0]); err != nil {
dir := filepath.Dir(dst)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(filepath.Dir(dst), perm[0]); err != nil {
if err = os.Chmod(dir, mode); err != nil {
return err
}
@ -712,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
// "application/json" --> JSON binding
// "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
// It decodes the payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
@ -769,8 +808,19 @@ func (c *Context) BindUri(obj any) error {
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
err := c.ShouldBindWith(obj, b)
if err != nil {
var maxBytesErr *http.MaxBytesError
// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
// https://github.com/goccy/go-json/issues/485
// https://github.com/bytedance/sonic/issues/800
switch {
case errors.As(err, &maxBytesErr):
c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
default:
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
}
return err
}
return nil
@ -782,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// "application/json" --> JSON binding
// "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
// It decodes the payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
@ -791,41 +841,71 @@ func (c *Context) ShouldBind(obj any) error {
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
//
// Example:
//
// POST /user
// Content-Type: application/json
//
// Request Body:
// {
// "name": "Manu",
// "age": 20
// }
//
// type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// var user User
// if err := c.ShouldBindJSON(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
// }
// c.JSON(http.StatusOK, user)
func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
// It works like ShouldBindJSON but binds query parameters from the URL.
func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// It works like ShouldBindJSON but binds plain text data from the request body.
func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
// It works like ShouldBindJSON but binds values from HTTP headers.
func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
// It works like ShouldBindJSON but binds parameters from the URI.
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params))
for _, v := range c.Params {
@ -889,7 +969,7 @@ func (c *Context) ShouldBindBodyWithPlain(obj any) error {
// ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
@ -909,18 +989,32 @@ func (c *Context) ClientIP() string {
}
}
var (
trusted bool
remoteIP net.IP
)
// If gin is listening a unix socket, always trust it.
localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
trusted = true
}
// Fallback
if !trusted {
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined by Engine.SetTrustedProxies()
remoteIP := net.ParseIP(c.RemoteIP())
remoteIP = net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
}
trusted := c.engine.isTrustedProxy(remoteIP)
trusted = c.engine.isTrustedProxy(remoteIP)
}
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
ip, valid := c.engine.validateHeader(headerValue)
if valid {
return ip
}
@ -962,9 +1056,10 @@ func (c *Context) requestHeader(key string) string {
/************************************/
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
// Uses http.StatusContinue constant for better code clarity.
func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
case status >= http.StatusContinue && status < http.StatusOK:
return false
case status == http.StatusNoContent:
return false
@ -1027,6 +1122,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
})
}
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
func (c *Context) SetCookieData(cookie *http.Cookie) {
if cookie.Path == "" {
cookie.Path = "/"
}
if cookie.SameSite == http.SameSiteDefaultMode {
cookie.SameSite = c.sameSite
}
http.SetCookie(c.Writer, cookie)
}
// Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found. And return the named cookie is unescaped.
// If multiple cookies match the given name, only one cookie will
@ -1116,6 +1224,12 @@ func (c *Context) XML(code int, obj any) {
c.Render(code, render.XML{Data: obj})
}
// PDF writes the given PDF binary data into the response body.
// It also sets the Content-Type as "application/pdf".
func (c *Context) PDF(code int, data []byte) {
c.Render(code, render.PDF{Data: data})
}
// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj any) {
c.Render(code, render.YAML{Data: obj})
@ -1131,6 +1245,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
c.Render(code, render.ProtoBuf{Data: obj})
}
// BSON serializes the given struct as BSON into the response body.
func (c *Context) BSON(code int, obj any) {
c.Render(code, render.BSON{Data: obj})
}
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...any) {
c.Render(code, render.String{Format: format, Data: values})
@ -1237,6 +1356,8 @@ type Negotiate struct {
YAMLData any
Data any
TOMLData any
PROTOBUFData any
BSONData any
}
// Negotiate calls different Render according to acceptable Accept format.
@ -1262,6 +1383,14 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data)
case binding.MIMEPROTOBUF:
data := chooseData(config.PROTOBUFData, config.Data)
c.ProtoBuf(code, data)
case binding.MIMEBSON:
data := chooseData(config.BSONData, config.Data)
c.BSON(code, data)
default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
}

35
context_file_test.go Normal file
View File

@ -0,0 +1,35 @@
package gin
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// TestContextFileSimple tests the Context.File() method with a simple case
func TestContextFileSimple(t *testing.T) {
// Test serving an existing file
testFile := "testdata/test_file.txt"
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File(testFile)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "This is a test file")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextFileNotFound tests serving a non-existent file
func TestContextFileNotFound(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File("non_existent_file.txt")
assert.Equal(t, http.StatusNotFound, w.Code)
}

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,9 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 21
const ginSupportMinGoVer = 25
var runtimeVersion = runtime.Version()
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -25,7 +27,7 @@ func IsDebugging() bool {
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
// DebugPrintFunc indicates debug log output format.
var DebugPrintFunc func(format string, values ...interface{})
var DebugPrintFunc func(format string, values ...any)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() {
@ -77,8 +79,8 @@ func getMinVer(v string) (uint64, error) {
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.22+.
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.25+.
`)
}

View File

@ -12,7 +12,6 @@ import (
"log"
"net/http"
"os"
"runtime"
"strings"
"sync"
"testing"
@ -21,10 +20,6 @@ import (
"github.com/stretchr/testify/require"
)
// TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) {
SetMode(DebugMode)
assert.True(t, IsDebugging())
@ -48,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
}
func TestDebugPrintFunc(t *testing.T) {
DebugPrintFunc = func(format string, values ...any) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrint("debug print func test: %d", 123)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
}
func TestDebugPrintError(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
debugPrintWARNINGDefault()
SetMode(TestMode)
})
m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.22+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
}
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
runtimeVersion = "go1.23.12"
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
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) {

16
doc.go
View File

@ -2,5 +2,21 @@
Package gin implements a HTTP web framework called gin.
See https://gin-gonic.com/ for more information about gin.
Example:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
*/
package gin // import "github.com/gin-gonic/gin"

View File

@ -22,6 +22,7 @@
- [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Avoid logging query strings](#avoid-loging-query-strings)
- [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string)
@ -63,6 +64,7 @@
- [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie)
- [Custom json codec at runtime](#custom-json-codec-at-runtime)
- [Don't trust all proxies](#dont-trust-all-proxies)
- [Testing](#testing)
@ -70,7 +72,7 @@
### Build with json replacement
Gin uses `encoding/json` as default json package but you can change it by build from other tags.
Gin uses `encoding/json` as the default JSON package but you can change it by building from other tags.
[jsoniter](https://github.com/json-iterator/go)
@ -84,10 +86,10 @@ go build -tags=jsoniter .
go build -tags=go_json .
```
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu support avx instruction.)
[sonic](https://github.com/bytedance/sonic)
```sh
$ go build -tags="sonic avx" .
$ go build -tags=sonic .
```
### Build without `MsgPack` rendering feature
@ -120,7 +122,7 @@ func main() {
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
// By default it serves on :8080 unless a
// By default, it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
@ -172,7 +174,7 @@ func main() {
router := gin.Default()
// Query string parameters are parsed using the existing underlying request object.
// The request responds to an url matching: /welcome?firstname=Jane&lastname=Doe
// The request responds to a URL matching: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
@ -300,7 +302,7 @@ curl -X POST http://localhost:8080/upload \
#### Multiple files
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
```go
func main() {
@ -591,6 +593,20 @@ func main() {
}
```
### Avoid logging query strings
```go
func main() {
router := gin.New()
// SkipQueryString indicates that the logger should not log the query string.
// For example, /path?q=1 will be logged as /path
loggerConfig := gin.LoggerConfig{SkipQueryString: true}
router.Use(gin.LoggerWithConfig(loggerConfig))
}
```
### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).
@ -704,7 +720,7 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
Skip validate: when running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
Skip-validation: Running the example above using the `curl` command returns an error. This is because the example uses `binding:"required"` for `Password`. If instead, you use `binding:"-"` for `Password`, then it will not return an error when you run the example again.
### Custom Validators
@ -832,6 +848,8 @@ type Person struct {
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
}
func main() {
@ -851,6 +869,8 @@ func startPage(c *gin.Context) {
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
log.Println(person.UnixMilliTime)
log.Println(person.UnixMicroTime)
}
c.String(http.StatusOK, "Success")
@ -860,7 +880,7 @@ func startPage(c *gin.Context) {
Test it with:
```sh
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
```
@ -868,7 +888,7 @@ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-1
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
```
```go
package main
import (
@ -906,7 +926,7 @@ curl -X POST http://localhost:8080/person
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
@ -1004,12 +1024,68 @@ curl -v localhost:8088/thinkerou/not-uuid
### Bind custom unmarshaler
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
```go
package main
import (
"github.com/gin-gonic/gin"
"encoding"
"strings"
"github.com/gin-gonic/gin"
)
type Birthday string
func (b *Birthday) UnmarshalText(text []byte) error {
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
return nil
}
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request)
})
_ = route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
```
Result
```sh
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
```
Note:
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
---
If a type already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead.
```go
package main
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type Birthday string
@ -1019,29 +1095,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
return nil
}
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday"`
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
ctx.JSON(200, request)
})
route.Run(":8088")
_ = route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01'
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
```
Result
```sh
"2000/01/01"
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
```
Note:
- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag.
- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client.
### Bind Header
```go
@ -1183,7 +1267,7 @@ func main() {
})
r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
// You can also use a struct
var msg struct {
Name string `json:"user"`
Message string
@ -1389,13 +1473,19 @@ func main() {
### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles()
Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go
//go:embed templates/*
var templates embed.FS
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
//or
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
@ -1486,7 +1576,7 @@ You may use custom delims
#### Custom Template Funcs
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/template).
main.go
@ -1538,7 +1628,7 @@ Date: 2017/07/01
### Multitemplate
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
Gin allows only one html.Template by default. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
@ -2087,7 +2177,7 @@ type formB struct {
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
// Calling c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
@ -2294,12 +2384,23 @@ func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
@ -2309,6 +2410,106 @@ func main() {
}
```
You can also use the `SetCookieData` method, which accepts a `*http.Cookie` directly for more flexibility:
```go
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
### Custom json codec at runtime
Gin support custom json serialization and deserialization logic without using compile tags.
1. Define a custom struct implements the `json.Core` interface.
2. Before your engine starts, assign values to `json.API` using the custom struct.
```go
package main
import (
"io"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/codec/json"
jsoniter "github.com/json-iterator/go"
)
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
// implement api.JsonApi
type customJsonApi struct {
}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
func main() {
//Replace the default json api
json.API = customJsonApi{}
//Start your gin engine
router := gin.Default()
router.Run(":8080")
}
```
## Don't trust all proxies
Gin lets you specify which headers to hold the real client IP (if any),
@ -2320,7 +2521,7 @@ or network CIDRs from where clients which their request headers related to clien
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs.
**Attention:** Gin trust all proxies by default if you don't specify a trusted
**Attention:** Gin trusts all proxies by default if you don't specify a trusted
proxy using the function above, **this is NOT safe**. At the same time, if you don't
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
then `Context.ClientIP()` will return the remote address directly to avoid some

View File

@ -9,7 +9,7 @@ import (
"reflect"
"strings"
"github.com/gin-gonic/gin/internal/json"
"github.com/gin-gonic/gin/codec/json"
)
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
@ -26,8 +26,6 @@ const (
ErrorTypePublic ErrorType = 1 << 1
// ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1
// ErrorTypeNu indicates any other error.
ErrorTypeNu = 2
)
// Error represents a error's specification.
@ -77,7 +75,7 @@ func (msg *Error) JSON() any {
// MarshalJSON implements the json.Marshaller interface.
func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON())
return json.API.Marshal(msg.JSON())
}
// Error implements the error interface.
@ -91,7 +89,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
}
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error {
func (msg Error) Unwrap() error {
return msg.Err
}
@ -157,7 +155,7 @@ func (a errorMsgs) JSON() any {
// MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON())
return json.API.Marshal(a.JSON())
}
func (a errorMsgs) String() string {

View File

@ -9,7 +9,7 @@ import (
"fmt"
"testing"
"github.com/gin-gonic/gin/internal/json"
"github.com/gin-gonic/gin/codec/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -33,8 +33,8 @@ func TestError(t *testing.T) {
"meta": "some data",
}, err.JSON())
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
jsonBytes, _ := json.API.Marshal(err)
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ //nolint: errcheck
"status": "200",
@ -92,14 +92,14 @@ Error #03: third
H{"error": "second", "meta": "some data"},
H{"error": "third", "status": "400"},
}, errs.JSON())
jsonBytes, _ := json.Marshal(errs)
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
jsonBytes, _ := json.API.Marshal(errs)
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
errs = errorMsgs{
{Err: errors.New("first"), Type: ErrorTypePrivate},
}
assert.Equal(t, H{"error": "first"}, errs.JSON())
jsonBytes, _ = json.Marshal(errs)
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
jsonBytes, _ = json.API.Marshal(errs)
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
errs = errorMsgs{}
assert.Nil(t, errs.Last())
@ -126,4 +126,15 @@ func TestErrorUnwrap(t *testing.T) {
require.ErrorIs(t, err, innerErr)
var testErr TestErr
require.ErrorAs(t, err, &testErr)
// Test non-pointer usage of gin.Error
errNonPointer := Error{
Err: innerErr,
Type: ErrorTypeAny,
}
wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer)
// Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage
require.ErrorIs(t, wrappedErr, innerErr)
var testErrNonPointer TestErr
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
}

1
fs.go
View File

@ -17,7 +17,6 @@ type OnlyFilesFS struct {
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
func (o OnlyFilesFS) Open(name string) (http.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}

99
gin.go
View File

@ -11,22 +11,23 @@ import (
"net/http"
"os"
"path"
"regexp"
"strings"
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
filesystem "github.com/gin-gonic/gin/internal/fs"
"github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
const defaultMultipartMemory = 32 << 20 // 32 MB
const escapedColon = "\\:"
const colon = ":"
const backslash = "\\"
const (
defaultMultipartMemory = 32 << 20 // 32 MB
escapedColon = "\\:"
colon = ":"
backslash = "\\"
)
var (
default404Body = []byte("404 page not found")
@ -46,9 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
},
}
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@ -94,6 +92,10 @@ const (
type Engine struct {
RouterGroup
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
routeTreesUpdated sync.Once
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
@ -133,10 +135,16 @@ type Engine struct {
AppEngine bool
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
// Only use RawPath if you know what you are doing.
UseRawPath bool
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
// It overrides UseRawPath
UseEscapedPath bool
// UnescapePathValues if true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
@ -189,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UseEscapedPath: false
// - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
@ -206,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
UseEscapedPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
@ -215,7 +225,7 @@ func New(opts ...OptionFunc) *Engine {
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.engine = engine
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
@ -285,6 +295,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ)
}
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
filesystem.FileSystem{FileSystem: fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
@ -321,7 +344,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine
}
// With returns a Engine with the configuration set in the OptionFunc.
// With returns an Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
@ -363,7 +386,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
}
// Routes returns a slice of registered routes, including some useful information, such as:
// the http method, path and the handler name.
// the http method, path, and the handler name.
func (engine *Engine) Routes() (routes RoutesInfo) {
for _, tree := range engine.trees {
routes = iterate("", tree.method, routes, tree.root)
@ -524,7 +547,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return
}
@ -540,7 +567,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
server := &http.Server{ // #nosec G112
Addr: addr,
Handler: engine.Handler(),
}
err = server.ListenAndServeTLS(certFile, keyFile)
return
}
@ -563,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close()
defer os.Remove(file)
err = http.Serve(listener, engine.Handler())
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return
}
@ -580,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
defer f.Close()
listener, err := net.FileListener(f)
if err != nil {
return
@ -616,12 +651,19 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.Serve(listener, engine.Handler())
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return
}
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.routeTreesUpdated.Do(func() {
engine.updateRouteTrees()
})
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
@ -637,17 +679,23 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
oldHandlers := c.handlers
c.reset()
engine.handleHTTPRequest(c)
c.index = oldIndexValue
c.handlers = oldHandlers
}
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
if engine.UseEscapedPath {
rPath = c.Request.URL.EscapedPath()
unescape = engine.UnescapePathValues
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
@ -734,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
req := c.Request
p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
prefix = regSafePrefix.ReplaceAllString(prefix, "")
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
prefix = sanitizePathChars(prefix)
prefix = removeRepeatedChar(prefix, '/')
p = prefix + "/" + req.URL.Path
}
@ -746,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
redirectRequest(c)
}
// sanitizePathChars removes unsafe characters from path strings,
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
func sanitizePathChars(s string) string {
return strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
return r
}
return -1
}, s)
}
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
rPath := req.URL.Path

View File

@ -12,15 +12,9 @@ import (
"github.com/gin-gonic/gin"
)
var once sync.Once
var internalEngine *gin.Engine
func engine() *gin.Engine {
once.Do(func() {
internalEngine = gin.Default()
})
return internalEngine
}
var engine = sync.OnceValue(func() *gin.Engine {
return gin.Default()
})
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
func LoadHTMLGlob(pattern string) {
@ -32,6 +26,11 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...)
}
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
engine().LoadHTMLFS(fs, patterns...)
}
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
@ -154,7 +153,7 @@ func RunUnix(file string) (err error) {
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
// Note: the method will block the calling goroutine indefinitely unless an error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}

246
ginS/gins_test.go Normal file
View File

@ -0,0 +1,246 @@
// 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 ginS
import (
"html/template"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func init() {
gin.SetMode(gin.TestMode)
}
func TestGET(t *testing.T) {
GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "test")
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "test", w.Body.String())
}
func TestPOST(t *testing.T) {
POST("/post", func(c *gin.Context) {
c.String(http.StatusCreated, "created")
})
req := httptest.NewRequest(http.MethodPost, "/post", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "created", w.Body.String())
}
func TestPUT(t *testing.T) {
PUT("/put", func(c *gin.Context) {
c.String(http.StatusOK, "updated")
})
req := httptest.NewRequest(http.MethodPut, "/put", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "updated", w.Body.String())
}
func TestDELETE(t *testing.T) {
DELETE("/delete", func(c *gin.Context) {
c.String(http.StatusOK, "deleted")
})
req := httptest.NewRequest(http.MethodDelete, "/delete", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "deleted", w.Body.String())
}
func TestPATCH(t *testing.T) {
PATCH("/patch", func(c *gin.Context) {
c.String(http.StatusOK, "patched")
})
req := httptest.NewRequest(http.MethodPatch, "/patch", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "patched", w.Body.String())
}
func TestOPTIONS(t *testing.T) {
OPTIONS("/options", func(c *gin.Context) {
c.String(http.StatusOK, "options")
})
req := httptest.NewRequest(http.MethodOptions, "/options", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "options", w.Body.String())
}
func TestHEAD(t *testing.T) {
HEAD("/head", func(c *gin.Context) {
c.String(http.StatusOK, "head")
})
req := httptest.NewRequest(http.MethodHead, "/head", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestAny(t *testing.T) {
Any("/any", func(c *gin.Context) {
c.String(http.StatusOK, "any")
})
req := httptest.NewRequest(http.MethodGet, "/any", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "any", w.Body.String())
}
func TestHandle(t *testing.T) {
Handle(http.MethodGet, "/handle", func(c *gin.Context) {
c.String(http.StatusOK, "handle")
})
req := httptest.NewRequest(http.MethodGet, "/handle", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "handle", w.Body.String())
}
func TestGroup(t *testing.T) {
group := Group("/group")
group.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "group test")
})
req := httptest.NewRequest(http.MethodGet, "/group/test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "group test", w.Body.String())
}
func TestUse(t *testing.T) {
var middlewareExecuted bool
Use(func(c *gin.Context) {
middlewareExecuted = true
c.Next()
})
GET("/middleware-test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/middleware-test", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.True(t, middlewareExecuted)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestNoRoute(t *testing.T) {
NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "custom 404")
})
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "custom 404", w.Body.String())
}
func TestNoMethod(t *testing.T) {
NoMethod(func(c *gin.Context) {
c.String(http.StatusMethodNotAllowed, "method not allowed")
})
// This just verifies that NoMethod is callable
// Testing the actual behavior would require a separate engine instance
assert.NotNil(t, engine())
}
func TestRoutes(t *testing.T) {
GET("/routes-test", func(c *gin.Context) {})
routes := Routes()
assert.NotEmpty(t, routes)
found := false
for _, route := range routes {
if route.Path == "/routes-test" && route.Method == http.MethodGet {
found = true
break
}
}
assert.True(t, found)
}
func TestSetHTMLTemplate(t *testing.T) {
tmpl := template.Must(template.New("test").Parse("Hello {{.}}"))
SetHTMLTemplate(tmpl)
// Verify engine has template set
assert.NotNil(t, engine())
}
func TestStaticFile(t *testing.T) {
StaticFile("/static-file", "../testdata/test_file.txt")
req := httptest.NewRequest(http.MethodGet, "/static-file", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestStatic(t *testing.T) {
Static("/static-dir", "../testdata")
req := httptest.NewRequest(http.MethodGet, "/static-dir/test_file.txt", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestStaticFS(t *testing.T) {
fs := http.Dir("../testdata")
StaticFS("/static-fs", fs)
req := httptest.NewRequest(http.MethodGet, "/static-fs/test_file.txt", nil)
w := httptest.NewRecorder()
engine().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}

View File

@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
@ -28,7 +29,6 @@ import (
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) {
if len(params) == 0 {
t.Fatal("url cannot be empty")
}
@ -47,12 +47,12 @@ func testRequest(t *testing.T, params ...string) {
body, ioerr := io.ReadAll(resp.Body)
require.NoError(t, ioerr)
var responseStatus = "200 OK"
responseStatus := "200 OK"
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
var responseBody = "it worked"
responseBody := "it worked"
if len(params) > 2 && params[2] != "" {
responseBody = params[2]
}
@ -70,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:8080/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example")
@ -170,7 +171,7 @@ func TestRunTLS(t *testing.T) {
}
func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(`
html := template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
@ -213,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:3123/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example")
@ -234,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run(":5150"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:5150/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example")
@ -262,10 +265,11 @@ func TestUnixSocket(t *testing.T) {
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -323,10 +327,11 @@ func TestFileDescriptor(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -355,10 +360,11 @@ func TestListener(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -394,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
var wg sync.WaitGroup
iterations := 200
wg.Add(iterations)
for i := 0; i < iterations; i++ {
for range iterations {
go func() {
req, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)

View File

@ -46,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt
})
})
})
@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(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 {
t.Error(err)
}
@ -325,6 +325,115 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
var tmplFS = http.Dir("testdata/template")
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
func TestAddRoute(t *testing.T) {
router := New()
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
@ -436,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
}
func TestRebuild404Handlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
// Initially, allNoRoute should be nil
assert.Nil(t, router.allNoRoute)
// Set NoRoute handlers
router.NoRoute(middleware0)
assert.Len(t, router.allNoRoute, 1)
assert.Len(t, router.noRoute, 1)
compareFunc(t, router.allNoRoute[0], middleware0)
// Add Use middleware should trigger rebuild404Handlers
router.Use(middleware1)
assert.Len(t, router.allNoRoute, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.noRoute, 1)
// Global middleware should come first
compareFunc(t, router.allNoRoute[0], middleware1)
compareFunc(t, router.allNoRoute[1], middleware0)
}
func TestNoMethodWithGlobalHandlers(t *testing.T) {
@ -573,6 +705,93 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is Escaped, the %25 is not interpreted as %
assert.Equal(t, "foo%252Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
r := New()
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is used, the %25 is interpreted as %
assert.Equal(t, "foo%2Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
assert.Equal(t, "foo%25bar", c.Param("path"))
c.Status(http.StatusOK)
})
assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
assert.Equal(t, 200, w.Code)
})
}
func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New()
@ -700,7 +919,7 @@ func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
fc := func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
@ -736,7 +955,7 @@ func TestWithOptionFunc(t *testing.T) {
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
return nil
}
@ -766,3 +985,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
func TestLiteralColonWithRun(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.updateRouteTrees()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHandler(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
handler := router.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHTTPServer(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.GET("/test/:param", func(c *Context) {
c.JSON(http.StatusOK, H{"param": c.Param("param")})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Contains(t, w2.Body.String(), "foo")
}
// Test that updateRouteTrees is called only once
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.String(http.StatusOK, "ok")
})
for range 5 {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ok", w.Body.String())
}
}

View File

@ -298,8 +298,8 @@ func TestShouldBindUri(t *testing.T) {
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
require.NoError(t, c.ShouldBindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
assert.NotEmpty(t, person.Name)
assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
@ -320,8 +320,8 @@ func TestBindUri(t *testing.T) {
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
require.NoError(t, c.BindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
assert.NotEmpty(t, person.Name)
assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "BindUri test OK")
})

60
go.mod
View File

@ -1,47 +1,45 @@
module github.com/gin-gonic/gin
go 1.22
go 1.25.0
require (
github.com/bytedance/sonic v1.11.6
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.20.0
github.com/goccy/go-json v0.10.2
github.com/bytedance/sonic v1.15.0
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.30.1
github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.19.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.2.2
github.com/quic-go/quic-go v0.48.2
github.com/stretchr/testify v1.9.0
github.com/ugorji/go/codec v1.2.12
golang.org/x/net v0.33.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1
github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.59.0
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.51.0
google.golang.org/protobuf v1.36.10
)
require gopkg.in/yaml.v3 v3.0.1 // indirect
require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

139
go.sum
View File

@ -1,115 +1,96 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -6,14 +6,17 @@ package bytesconv
import (
"bytes"
cRand "crypto/rand"
"math/rand"
"strings"
"testing"
"time"
)
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
var testBytes = []byte(testString)
var (
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
testBytes = []byte(testString)
)
func rawBytesToStr(b []byte) string {
return string(b)
@ -27,14 +30,26 @@ func rawStrToBytes(s string) []byte {
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
rand.Read(data)
for range 100 {
_, err := cRand.Read(data)
if err != nil {
t.Fatal(err)
}
if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match")
}
}
}
func TestBytesToStringEmpty(t *testing.T) {
if got := BytesToString([]byte{}); got != "" {
t.Fatalf("BytesToString([]byte{}) = %q; want empty string", got)
}
if got := BytesToString(nil); got != "" {
t.Fatalf("BytesToString(nil) = %q; want empty string", got)
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
@ -42,7 +57,7 @@ const (
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
var src = rand.New(rand.NewSource(time.Now().UnixNano()))
func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{}
@ -64,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
}
func TestStringToBytes(t *testing.T) {
for i := 0; i < 100; i++ {
for range 100 {
s := RandStringBytesMaskImprSrcSB(64)
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
t.Fatal("don't match")
@ -72,28 +87,38 @@ func TestStringToBytes(t *testing.T) {
}
}
func TestStringToBytesEmpty(t *testing.T) {
b := StringToBytes("")
if len(b) != 0 {
t.Fatalf(`StringToBytes("") length = %d; want 0`, len(b))
}
if !bytes.Equal(b, []byte("")) {
t.Fatalf(`StringToBytes("") = %v; want []byte("")`, b)
}
}
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
rawBytesToStr(testBytes)
}
}
func BenchmarkBytesConvBytesToStr(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
BytesToString(testBytes)
}
}
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
rawStrToBytes(testString)
}
}
func BenchmarkBytesConvStrToBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
StringToBytes(testString)
}
}

21
internal/fs/fs.go Normal file
View File

@ -0,0 +1,21 @@
package fs
import (
"io/fs"
"net/http"
)
// FileSystem implements an [fs.FS].
type FileSystem struct {
http.FileSystem
}
// Open passes `Open` to the upstream implementation and return an [fs.File].
func (o FileSystem) Open(name string) (fs.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
return fs.File(f), nil
}

49
internal/fs/fs_test.go Normal file
View File

@ -0,0 +1,49 @@
package fs
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestFileSystem_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file)
}
func TestFileSystem_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &FileSystem{mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}

View File

@ -1,22 +0,0 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go_json
package json
import json "github.com/goccy/go-json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -1,22 +0,0 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !jsoniter && !go_json && !(sonic && avx && (linux || windows || darwin) && amd64)
package json
import "encoding/json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -1,23 +0,0 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build jsoniter
package json
import jsoniter "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -1,23 +0,0 @@
// Copyright 2022 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.
//go:build sonic && avx && (linux || windows || darwin) && amd64
package json
import "github.com/bytedance/sonic"
var (
json = sonic.ConfigStd
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -44,10 +44,15 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
// SkipPaths is an url path array which logs are not written.
// SkipPaths is a URL path array which logs are not written.
// Optional.
SkipPaths []string
// SkipQueryString indicates that query strings should not be written
// for cases such as when API keys are passed via query strings.
// Optional. Default value is false.
SkipQueryString bool
// Skip is a Skipper that indicates which logs should not be written.
// Optional.
Skip Skipper
@ -82,7 +87,7 @@ type LogFormatterParams struct {
// BodySize is the size of the Response Body
BodySize int
// Keys are the keys set on the request's context.
Keys map[string]any
Keys map[any]any
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
@ -103,6 +108,27 @@ func (p *LogFormatterParams) StatusCodeColor() string {
}
}
// LatencyColor is the ANSI color for latency
func (p *LogFormatterParams) LatencyColor() string {
latency := p.Latency
switch {
case latency < time.Millisecond*100:
return white
case latency < time.Millisecond*200:
return green
case latency < time.Millisecond*300:
return cyan
case latency < time.Millisecond*500:
return blue
case latency < time.Second:
return yellow
case latency < time.Second*2:
return magenta
default:
return red
}
}
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
func (p *LogFormatterParams) MethodColor() string {
method := p.Method
@ -139,20 +165,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
var statusColor, methodColor, resetColor, latencyColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
latencyColor = param.LatencyColor()
}
if param.Latency > time.Minute {
param.Latency = param.Latency.Truncate(time.Second)
switch {
case param.Latency > time.Minute:
param.Latency = param.Latency.Truncate(time.Second * 10)
case param.Latency > time.Second:
param.Latency = param.Latency.Truncate(time.Millisecond * 10)
case param.Latency > time.Millisecond:
param.Latency = param.Latency.Truncate(time.Microsecond * 10)
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
return fmt.Sprintf("[GIN] %v |%s %3d %s|%s %8v %s| %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
latencyColor, param.Latency, resetColor,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
@ -270,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param.BodySize = c.Writer.Size()
if raw != "" {
if raw != "" && !conf.SkipQueryString {
path = path + "?" + raw
}

View File

@ -39,7 +39,7 @@ func TestLogger(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
// than individual functions. I'm not sure where these should go.
buffer.Reset()
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
@ -103,7 +103,7 @@ func TestLoggerWithConfig(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
// than individual functions. I'm not sure where these should go.
buffer.Reset()
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
@ -181,7 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
var gotKeys map[string]any
var gotKeys map[any]any
buffer := new(strings.Builder)
router := New()
@ -278,10 +278,10 @@ func TestDefaultLogFormatter(t *testing.T) {
}
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
}
func TestColorForMethod(t *testing.T) {
@ -317,6 +317,24 @@ func TestColorForStatus(t *testing.T) {
assert.Equal(t, red, colorForStatus(2), "other things should be red")
}
func TestColorForLatency(t *testing.T) {
colorForLatency := func(latency time.Duration) string {
p := LogFormatterParams{
Latency: latency,
}
return p.LatencyColor()
}
assert.Equal(t, white, colorForLatency(time.Duration(0)), "0 should be white")
assert.Equal(t, white, colorForLatency(time.Millisecond*20), "20ms should be white")
assert.Equal(t, green, colorForLatency(time.Millisecond*150), "150ms should be green")
assert.Equal(t, cyan, colorForLatency(time.Millisecond*250), "250ms should be cyan")
assert.Equal(t, blue, colorForLatency(time.Millisecond*400), "400ms should be blue")
assert.Equal(t, yellow, colorForLatency(time.Millisecond*600), "600ms should be yellow")
assert.Equal(t, magenta, colorForLatency(time.Millisecond*1500), "1.5s should be magenta")
assert.Equal(t, red, colorForLatency(time.Second*3), "other things should be red")
}
func TestResetColor(t *testing.T) {
p := LogFormatterParams{}
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
@ -371,11 +389,11 @@ func TestErrorLogger(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -454,3 +472,17 @@ func TestForceConsoleColor(t *testing.T) {
// reset console color mode.
consoleColorMode = autoColor
}
func TestLoggerWithConfigSkipQueryString(t *testing.T) {
buffer := new(strings.Builder)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipQueryString: true,
}))
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
PerformRequest(router, "GET", "/logged?a=21")
assert.Contains(t, buffer.String(), "200")
assert.NotContains(t, buffer.String(), "a=21")
}

View File

@ -203,7 +203,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
assert.Equal(t, "ACB", signature)
}
// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// as well as Abort
func TestMiddlewareFailHandlersChain(t *testing.T) {
// SETUP
@ -249,5 +249,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
}

View File

@ -44,8 +44,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
var ginMode int32 = debugCode
var modeName atomic.Value
var (
ginMode int32 = debugCode
modeName atomic.Value
)
func init() {
mode := os.Getenv(EnvGinMode)
@ -63,7 +65,7 @@ func SetMode(value string) {
}
switch value {
case DebugMode, "":
case DebugMode:
atomic.StoreInt32(&ginMode, debugCode)
case ReleaseMode:
atomic.StoreInt32(&ginMode, releaseCode)

55
path.go
View File

@ -5,6 +5,8 @@
package gin
const stackBufSize = 128
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
//
@ -19,7 +21,6 @@ package gin
//
// If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/"
if p == "" {
return "/"
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
}
b[w] = c
}
// removeRepeatedChar removes multiple consecutive 'char's from a string.
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
func removeRepeatedChar(s string, char byte) string {
// Check if there are any consecutive chars
hasRepeatedChar := false
for i := 1; i < len(s); i++ {
if s[i] == char && s[i-1] == char {
hasRepeatedChar = true
break
}
}
if !hasRepeatedChar {
return s
}
// Reasonably sized buffer on stack to avoid allocations in the common case.
buf := make([]byte, 0, stackBufSize)
// Invariants:
// reading from s; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
r := 0
w := 0
for n := len(s); r < n; {
if s[r] == char {
// Write the first char
bufApp(&buf, s, w, char)
w++
r++
// Skip all consecutive chars
for r < n && s[r] == char {
r++
}
} else {
// Copy non-char character
bufApp(&buf, s, w, s[r])
w++
r++
}
}
// If the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise, return a new string from the buffer.
if len(buf) == 0 {
return s[:w]
}
return string(buf[:w])
}

View File

@ -94,7 +94,7 @@ func TestPathCleanMallocs(t *testing.T) {
func BenchmarkPathClean(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
for _, test := range cleanTests {
cleanPath(test.path)
}
@ -134,12 +134,59 @@ func TestPathCleanLong(t *testing.T) {
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}
func TestRemoveRepeatedChar(t *testing.T) {
testCases := []struct {
name string
str string
char byte
want string
}{
{
name: "empty",
str: "",
char: 'a',
want: "",
},
{
name: "noSlash",
str: "abc",
char: ',',
want: "abc",
},
{
name: "withSlash",
str: "/a/b/c/",
char: '/',
want: "/a/b/c/",
},
{
name: "withRepeatedSlashes",
str: "/a//b///c////",
char: '/',
want: "/a/b/c/",
},
{
name: "threeSlashes",
str: "///",
char: '/',
want: "/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := removeRepeatedChar(tc.str, tc.char)
assert.Equal(t, tc.want, res)
})
}
}

View File

@ -5,25 +5,27 @@
package gin
import (
"bufio"
"bytes"
"cmp"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime"
"strings"
"syscall"
"time"
"github.com/gin-gonic/gin/internal/bytesconv"
)
var (
dunno = []byte("???")
centerDot = []byte("·")
dot = []byte(".")
slash = []byte("/")
const (
dunno = "???"
stackSkip = 3
)
// RecoveryFunc defines the function passable to CustomRecovery.
@ -55,47 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
if rec := recover(); rec != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
var se *os.SyscallError
if errors.As(ne, &se) {
seStr := strings.ToLower(se.Error())
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true
}
}
var isBrokenPipe bool
err, ok := rec.(error)
if ok {
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ECONNRESET) ||
errors.Is(err, http.ErrAbortHandler)
}
if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe {
logger.Printf("%s\n%s%s", err, headersToStr, reset)
if isBrokenPipe {
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), headersToStr, err, stack, reset)
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
timeFormat(time.Now()), rec, stack(stackSkip), reset)
}
}
if brokenPipe {
if isBrokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck
c.Error(err) //nolint: errcheck
c.Abort()
} else {
handle(c, err)
handle(c, rec)
}
}
}()
@ -103,6 +91,21 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
}
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
//
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
func secureRequestDump(r *http.Request) string {
httpRequest, _ := httputil.DumpRequest(r, false)
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
for i, line := range lines {
if strings.HasPrefix(line, "Authorization:") {
lines[i] = "Authorization: *"
}
}
return strings.Join(lines, "\r\n")
}
func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError)
}
@ -112,8 +115,11 @@ func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently
// loaded file.
var lines [][]byte
var lastFile string
var (
nLine string
lastFile string
err error
)
for i := skip; ; i++ { // Skip the expected number of frames
pc, file, line, ok := runtime.Caller(i)
if !ok {
@ -122,34 +128,53 @@ func stack(skip int) []byte {
// 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)
if file != lastFile {
data, err := os.ReadFile(file)
nLine, err = readNthLine(file, line-1)
if err != nil {
continue
}
lines = bytes.Split(data, []byte{'\n'})
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()
}
// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
if n < 0 || n >= len(lines) {
return dunno
// readNthLine reads the nth line from the file.
// It returns the trimmed content of the line if found,
// or an empty string if the line doesn't exist.
// If there's an error opening the file, it returns the error.
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.
func function(pc uintptr) []byte {
func function(pc uintptr) string {
fn := runtime.FuncForPC(pc)
if fn == nil {
return dunno
}
name := []byte(fn.Name())
name := fn.Name()
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
@ -158,13 +183,13 @@ func function(pc uintptr) []byte {
// *T.ptrmethod
// Also the package path might contain dot (e.g. code.google.com/...),
// so first eliminate the path prefix
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
name = name[lastSlash+1:]
}
if period := bytes.Index(name, dot); period >= 0 {
if period := strings.IndexByte(name, '.'); period >= 0 {
name = name[period+1:]
}
name = bytes.ReplaceAll(name, centerDot, dot)
name = strings.ReplaceAll(name, "·", ".")
return name
}

View File

@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery",
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
router := New()
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, dunno, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, dunno, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
}
func TestFunction(t *testing.T) {
bs := function(1)
assert.Equal(t, dunno, bs)
@ -113,13 +98,13 @@ func TestFunction(t *testing.T) {
func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204
expectMsgs := map[syscall.Errno]string{
syscall.EPIPE: "broken pipe",
syscall.ECONNRESET: "connection reset by peer",
expectErrnos := []syscall.Errno{
syscall.EPIPE,
syscall.ECONNRESET,
}
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
for _, errno := range expectErrnos {
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
var buf strings.Builder
router := New()
@ -137,11 +122,36 @@ func TestPanicWithBrokenPipe(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
})
}
}
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
func TestPanicWithAbortHandler(t *testing.T) {
const expectCode = 204
var buf strings.Builder
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Panic with ErrAbortHandler which should be treated as broken pipe
panic(http.ErrAbortHandler)
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
out := buf.String()
assert.Contains(t, out, "net/http: abort Handler")
assert.NotContains(t, out, "panic recovered")
}
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)
@ -152,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
}
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -171,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
@ -187,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
}
router.Use(CustomRecovery(handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -206,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
@ -222,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
}
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -241,7 +251,119 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
func TestSecureRequestDump(t *testing.T) {
tests := []struct {
name string
req *http.Request
wantContains string
wantNotContain string
}{
{
name: "Authorization header standard case",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("Authorization", "Bearer secret-token")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "Bearer secret-token",
},
{
name: "authorization header lowercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("authorization", "some-secret")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "some-secret",
},
{
name: "Authorization header mixed case",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("AuThOrIzAtIoN", "token123")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "token123",
},
{
name: "No Authorization header",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("Content-Type", "application/json")
return r
}(),
wantContains: "",
wantNotContain: "Authorization: *",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := secureRequestDump(tt.req)
if tt.wantContains != "" && !strings.Contains(result, tt.wantContains) {
t.Errorf("maskHeaders() = %q, want contains %q", result, tt.wantContains)
}
if tt.wantNotContain != "" && strings.Contains(result, tt.wantNotContain) {
t.Errorf("maskHeaders() = %q, want NOT contain %q", result, tt.wantNotContain)
}
})
}
}
// 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
View 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)
}

View File

@ -4,7 +4,10 @@
package render
import "net/http"
import (
"net/http"
"strconv"
)
// Data contains ContentType and bytes data.
type Data struct {
@ -15,6 +18,9 @@ type Data struct {
// Render (Data) writes data with custom ContentType.
func (r Data) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
if len(r.Data) > 0 {
w.Header().Set("Content-Length", strconv.Itoa(len(r.Data)))
}
_, err = w.Write(r.Data)
return
}

View File

@ -7,6 +7,8 @@ package render
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin/internal/fs"
)
// Delims represents a set of Left and Right delimiters for HTML template rendering.
@ -33,6 +35,8 @@ type HTMLProduction struct {
type HTMLDebug struct {
Files []string
Glob string
FileSystem http.FileSystem
Patterns []string
Delims Delims
FuncMap template.FuncMap
}
@ -63,6 +67,7 @@ func (r HTMLDebug) Instance(name string, data any) Render {
Data: data,
}
}
func (r HTMLDebug) loadTemplate() *template.Template {
if r.FuncMap == nil {
r.FuncMap = template.FuncMap{}
@ -73,7 +78,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
panic("the HTML debug render was created without files or glob pattern")
if r.FileSystem != nil && len(r.Patterns) > 0 {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
}
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
}
// Render (HTML) executes template and writes its result with custom ContentType for response.

View File

@ -9,9 +9,10 @@ import (
"fmt"
"html/template"
"net/http"
"unicode"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
)
// JSON contains the given interface object.
@ -65,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj any) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
jsonBytes, err := json.API.Marshal(obj)
if err != nil {
return err
}
@ -76,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
}
@ -92,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data)
jsonBytes, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
@ -115,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
ret, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
@ -151,20 +152,23 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
}
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
func (r AsciiJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
ret, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
var buffer bytes.Buffer
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r)
if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r))
if r > unicode.MaxASCII {
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
buffer.Write(escapeBuf)
} else {
buffer.WriteByte(byte(r))
}
buffer.WriteString(cvt)
}
_, err = w.Write(buffer.Bytes())
@ -179,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
encoder := json.NewEncoder(w)
encoder := json.API.NewEncoder(w)
encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data)
}

26
render/pdf.go Normal file
View 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)
}

View File

@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
}
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers)
r.writeHeaders(w)
_, err = io.Copy(w, r.Reader)
return
}
@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}
// writeHeaders writes custom Header.
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
// writeHeaders writes headers from r.Headers into response.
func (r Reader) writeHeaders(w http.ResponseWriter) {
header := w.Header()
for k, v := range headers {
for k, v := range r.Headers {
if header.Get(k) == "" {
header.Set(k, v)
}

View File

@ -31,6 +31,7 @@ var (
_ Render = (*AsciiJSON)(nil)
_ Render = (*ProtoBuf)(nil)
_ Render = (*TOML)(nil)
_ Render = (*PDF)(nil)
)
func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -7,7 +7,7 @@
package render
import (
"bytes"
"errors"
"net/http/httptest"
"testing"
@ -16,9 +16,6 @@ import (
"github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) {
require.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
var decoded map[string]any
var mh codec.MsgpackHandle
mh.RawToString = true
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
require.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, data, decoded)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestWriteMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
"num": 42,
}
err := WriteMsgPack(w, data)
require.NoError(t, err)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
var decoded map[string]any
var mh codec.MsgpackHandle
mh.RawToString = true
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
require.NoError(t, err)
assert.Len(t, decoded, 2)
assert.Equal(t, "bar", decoded["foo"])
assert.EqualValues(t, 42, decoded["num"])
}
type failWriter struct {
*httptest.ResponseRecorder
}
func (w *failWriter) Write(data []byte) (int, error) {
return 0, errors.New("write error")
}
func TestRenderMsgPackError(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
}
err := (MsgPack{data}).Render(&failWriter{w})
require.Error(t, err)
assert.Contains(t, err.Error(), "write error")
}

View File

@ -8,6 +8,7 @@ import (
"encoding/xml"
"errors"
"html/template"
"io"
"net"
"net/http"
"net/http/httptest"
@ -15,16 +16,13 @@ import (
"strings"
"testing"
"github.com/gin-gonic/gin/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
// TODO unit tests
// test errors
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
@ -38,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -60,7 +58,7 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -85,7 +83,7 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1)
require.NoError(t, err1)
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
@ -140,18 +138,43 @@ func TestRenderJsonpJSON(t *testing.T) {
type errorWriter struct {
bufString string
ErrThreshold int // 1-based threshold. If 1, errors on the 1st Write call.
writeCount int
*httptest.ResponseRecorder
}
var _ http.ResponseWriter = (*errorWriter)(nil)
func (w *errorWriter) Header() http.Header {
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
return w.ResponseRecorder.Header()
}
func (w *errorWriter) WriteHeader(statusCode int) {
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
w.ResponseRecorder.WriteHeader(statusCode)
}
func (w *errorWriter) Write(buf []byte) (int, error) {
if string(buf) == w.bufString {
return 0, errors.New(`write "` + w.bufString + `" error`)
w.writeCount++
if (w.bufString != "" && string(buf) == w.bufString) || (w.ErrThreshold > 0 && w.writeCount >= w.ErrThreshold) {
return 0, errors.New(`write error`)
}
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
return w.ResponseRecorder.Write(buf)
}
func (w *errorWriter) reset() {
w.writeCount = 0
w.ResponseRecorder = httptest.NewRecorder()
}
func TestRenderJsonpJSONError(t *testing.T) {
ew := &errorWriter{
ResponseRecorder: httptest.NewRecorder(),
@ -164,23 +187,33 @@ func TestRenderJsonpJSONError(t *testing.T) {
},
}
cb := template.JSEscapeString(jsonpJSON.Callback)
ew.bufString = cb
err := jsonpJSON.Render(ew) // error was returned while writing callback
assert.Equal(t, `write "`+cb+`" error`, err.Error())
// error was returned while writing callback
ew.reset()
ew.ErrThreshold = 1
err := jsonpJSON.Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
ew.bufString = `(`
// error was returned while writing "("
ew.reset()
ew.ErrThreshold = 2
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
ew.bufString = string(data)
// error was returned while writing data
ew.reset()
ew.ErrThreshold = 3
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
ew.bufString = `);`
// error was returned while writing ");"
ew.reset()
ew.ErrThreshold = 4
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderJsonpJSONError2(t *testing.T) {
@ -194,7 +227,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
e := (JsonpJSON{"", data}).Render(w)
require.NoError(t, e)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -217,7 +250,7 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1)
require.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
@ -244,7 +277,7 @@ func TestRenderPureJSON(t *testing.T) {
}
err := (PureJSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -285,7 +318,14 @@ b:
err := (YAML{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3
// We're checking that the output contains the expected data, not the exact formatting
output := w.Body.String()
assert.Contains(t, output, "a : Easy!")
assert.Contains(t, output, "b:")
assert.Contains(t, output, "c: 2")
assert.Contains(t, output, "d: [3, 4]")
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -352,6 +392,55 @@ func TestRenderProtoBufFail(t *testing.T) {
require.Error(t, err)
}
func TestRenderBSON(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
type mystruct struct {
Label string
Reps []int64
}
data := &mystruct{
Label: "test",
Reps: reps,
}
(BSON{data}).WriteContentType(w)
bsonData, err := bson.Marshal(data)
require.NoError(t, err)
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
err = (BSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, bsonData, w.Body.Bytes())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestRenderBSONError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
err := (BSON{data}).Render(w)
require.Error(t, err)
}
func TestRenderBSONWriteError(t *testing.T) {
type testStruct struct {
Value string
}
data := &testStruct{Value: "test"}
ew := &errorWriter{
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
err := (BSON{data}).Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
data := xmlmap{
@ -368,6 +457,31 @@ func TestRenderXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderXMLError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
err := (XML{data}).Render(w)
require.Error(t, err)
assert.Contains(t, err.Error(), "xml: unsupported type: chan int")
}
func TestRenderPDF(t *testing.T) {
w := httptest.NewRecorder()
data := []byte("%Test pdf content")
pdf := PDF{data}
pdf.WriteContentType(w)
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
err := pdf.Render(w)
require.NoError(t, err)
assert.Equal(t, data, w.Body.Bytes())
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
}
func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
require.NoError(t, err)
@ -420,6 +534,52 @@ func TestRenderData(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, "19", w.Header().Get("Content-Length"))
}
func TestRenderDataContentLength(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size, err := strconv.Atoi(r.URL.Query().Get("size"))
assert.NoError(t, err)
data := Data{
ContentType: "application/octet-stream",
Data: make([]byte, size),
}
assert.NoError(t, data.Render(w))
}))
t.Cleanup(srv.Close)
for _, size := range []int{0, 1, 100, 100_000} {
t.Run(strconv.Itoa(size), func(t *testing.T) {
resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type"))
assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length"))
actual, err := io.Copy(io.Discard, resp.Body)
require.NoError(t, err)
assert.EqualValues(t, size, actual)
})
}
}
func TestRenderDataError(t *testing.T) {
ew := &errorWriter{
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
data := []byte("#!PNG some raw data")
err := (Data{
ContentType: "image/png",
Data: data,
}).Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderString(t *testing.T) {
@ -491,6 +651,8 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
@ -510,6 +672,29 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
err := instance.Render(w)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFS(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: http.Dir("../testdata/template"),
Patterns: []string{"hello.tmpl"},
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
@ -528,12 +713,40 @@ func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{"{{", "}}"},
FuncMap: 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) {
w := httptest.NewRecorder()
@ -581,14 +794,14 @@ func TestRenderReaderNoContentLength(t *testing.T) {
}
func TestRenderWriteError(t *testing.T) {
data := []interface{}{"value1", "value2"}
data := []any{"value1", "value2"}
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
bufString: prefix,
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
require.Error(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
assert.Equal(t, "write error", err.Error())
}

View File

@ -15,7 +15,7 @@ type TOML struct {
Data any
}
var TOMLContentType = []string{"application/toml; charset=utf-8"}
var tomlContentType = []string{"application/toml; charset=utf-8"}
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
func (r TOML) Render(w http.ResponseWriter) error {
@ -32,5 +32,5 @@ func (r TOML) Render(w http.ResponseWriter) error {
// WriteContentType (TOML) writes TOML ContentType for response.
func (r TOML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, TOMLContentType)
writeContentType(w, tomlContentType)
}

View File

@ -7,7 +7,7 @@ package render
import (
"net/http"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
)
// YAML contains the given interface object.

View File

@ -6,6 +6,7 @@ package gin
import (
"bufio"
"errors"
"io"
"net"
"net/http"
@ -16,6 +17,8 @@ const (
defaultStatus = http.StatusOK
)
var errHijackAlreadyWritten = errors.New("gin: response body already written")
// ResponseWriter ...
type ResponseWriter interface {
http.ResponseWriter
@ -106,6 +109,11 @@ func (w *responseWriter) Written() bool {
// Hijack implements the http.Hijacker interface.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// Allow hijacking before any data is written (size == -1) or after headers are written (size == 0),
// but not after body data is written (size > 0). For compatibility with websocket libraries (e.g., github.com/coder/websocket)
if w.size > 0 {
return nil, nil, errHijackAlreadyWritten
}
if w.size < 0 {
w.size = 0
}
@ -120,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flusher interface.
func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {

View File

@ -5,6 +5,8 @@
package gin
import (
"bufio"
"net"
"net/http"
"net/http/httptest"
"testing"
@ -124,6 +126,132 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush()
}
type mockHijacker struct {
*httptest.ResponseRecorder
hijacked bool
}
// Hijack implements the http.Hijacker interface. It just records that it was called.
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
m.hijacked = true
return nil, nil, nil
}
func TestResponseWriterHijackAfterWrite(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error // Action to perform before hijacking
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack before write should succeed",
action: func(w ResponseWriter) error { return nil },
expectWrittenBeforeHijack: false,
expectHijackSuccess: true,
expectWrittenAfterHijack: true, // Hijack itself marks the writer as written
expectError: nil,
},
{
name: "hijack after write should fail",
action: func(w ResponseWriter) error {
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
// Check initial state
assert.False(t, w.Written(), "should not be written initially")
// Perform pre-hijack action
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
// Check state before hijacking
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
// Attempt to hijack
_, _, hijackErr := w.Hijack()
// Check results
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
action: func(w ResponseWriter) error {
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
return nil
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
expectWrittenAfterHijack: true,
expectError: nil,
},
{
name: "hijack after WriteHeaderNow + Write should fail",
action: func(w ResponseWriter) error {
w.WriteHeaderNow()
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
_, _, hijackErr := w.Hijack()
if tc.expectError == nil {
require.NoError(t, hijackErr, "expected hijack to succeed")
} else {
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
}
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{}

View File

@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
})
}
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
// Gin by default uses: gin.Dir()
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {

View File

@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/assert"
)
var MaxHandlers = 32
func init() {
SetMode(TestMode)
}
@ -193,3 +195,25 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.Static("/static", "."))
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
}
func TestRouterGroupCombineHandlersTooManyHandlers(t *testing.T) {
group := &RouterGroup{
Handlers: make(HandlersChain, MaxHandlers), // Assume group already has MaxHandlers middleware
}
tooManyHandlers := make(HandlersChain, MaxHandlers) // Add MaxHandlers more, total 2 * MaxHandlers
// This should trigger panic
assert.Panics(t, func() {
group.combineHandlers(tooManyHandlers)
}, "should panic due to too many handlers")
}
func TestRouterGroupCombineHandlersEmptySliceNotNil(t *testing.T) {
group := &RouterGroup{
Handlers: HandlersChain{},
}
result := group.combineHandlers(HandlersChain{})
assert.NotNil(t, result, "result should not be nil even with empty handlers")
assert.Empty(t, result, "empty handlers should return empty chain")
}

View File

@ -484,7 +484,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Contains(t, w.Body.String(), "package gin")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEmpty(t, w.Header().Get("Content-Type"))
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified"))
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@ -764,7 +764,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
// Test not found
router.Use(func(c *Context) {
// For not found routes full path is empty
assert.Equal(t, "", c.FullPath())
assert.Empty(t, c.FullPath())
})
w := PerformRequest(router, http.MethodGet, "/not-found")

View File

@ -4,9 +4,16 @@
package gin
import "net/http"
import (
"fmt"
"net/http"
"time"
)
// CreateTestContext returns a fresh engine and context for testing purposes
// CreateTestContext returns a fresh Engine and a Context associated with it.
// This is useful for tests that need to set up a new Gin engine instance
// along with a context, for example, to test middleware that doesn't depend on
// specific routes. The ResponseWriter `w` is used to initialize the context's writer.
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
r = New()
c = r.allocateContext(0)
@ -15,10 +22,39 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
return
}
// CreateTestContextOnly returns a fresh context base on the engine for testing purposes
// CreateTestContextOnly returns a fresh Context associated with the provided Engine `r`.
// This is useful for tests that operate on an existing, possibly pre-configured,
// Gin engine instance and need a new context for it.
// The ResponseWriter `w` is used to initialize the context's writer.
// The context is allocated with the `maxParams` setting from the provided engine.
func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c = r.allocateContext(r.maxParams)
c.reset()
c.writermem.reset(w)
return
}
// waitForServerReady waits for a server to be ready by making HTTP requests
// with exponential backoff. This is more reliable than time.Sleep() for testing.
func waitForServerReady(url string, maxAttempts int) error {
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
for i := 0; i < maxAttempts; i++ {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
return nil
}
// Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms...
backoff := time.Duration(10*(1<<uint(i))) * time.Millisecond
if backoff > 500*time.Millisecond {
backoff = 500 * time.Millisecond
}
time.Sleep(backoff)
}
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
}

2
testdata/test_file.txt vendored Normal file
View File

@ -0,0 +1,2 @@
This is a test file for Context.File() method testing.
It contains some sample content to verify file serving functionality.

98
tree.go
View File

@ -5,7 +5,6 @@
package gin
import (
"bytes"
"net/url"
"strings"
"unicode"
@ -14,12 +13,6 @@ import (
"github.com/gin-gonic/gin/internal/bytesconv"
)
var (
strColon = []byte(":")
strStar = []byte("*")
strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value.
type Param struct {
Key string
@ -85,16 +78,13 @@ func (n *node) addChild(child *node) {
}
func countParams(path string) uint16 {
var n uint16
s := bytesconv.StringToBytes(path)
n += uint16(bytes.Count(s, strColon))
n += uint16(bytes.Count(s, strStar))
return n
colons := strings.Count(path, ":")
stars := strings.Count(path, "*")
return safeUint16(colons + stars)
}
func countSections(path string) uint16 {
s := bytesconv.StringToBytes(path)
return uint16(bytes.Count(s, strSlash))
return safeUint16(strings.Count(path, "/"))
}
type nodeType uint8
@ -234,7 +224,7 @@ walk:
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
pathSeg, _, _ = strings.Cut(pathSeg, "/")
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
@ -358,7 +348,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := ""
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
}
panic("catch-all wildcard '" + path +
"' in new path '" + fullPath +
@ -383,7 +373,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
}
n.addChild(child)
n.indices = string('/')
n.indices = "/"
n = child
n.priority++
@ -681,12 +671,7 @@ walk: // Outer loop for walking the tree
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
const stackBufSize = 128
// Use a static sized buffer on the stack in the common case.
// If the path is too long, allocate a buffer on the heap instead.
buf := make([]byte, 0, stackBufSize)
if length := len(path) + 1; length > stackBufSize {
buf = make([]byte, 0, length)
}
buf := make([]byte, 0, max(stackBufSize, len(path)+1))
ciPath := n.findCaseInsensitivePathRec(
path,
@ -833,7 +818,72 @@ walk: // Outer loop for walking the tree
return nil
}
n = n.children[0]
// When wildChild is true, try static children first (via indices)
// before falling back to the wildcard child. This ensures that
// case-insensitive lookups prefer static routes over param routes
// (e.g., /PREFIX/XXX should resolve to /prefix/xxx, not match :id).
if len(n.indices) > 0 {
rb = shiftNRuneBytes(rb, npLen)
if rb[0] != 0 {
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
} else {
var rv rune
var off int
for max_ := min(npLen, 3); off < max_; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
break
}
}
lo := unicode.ToLower(rv)
utf8.EncodeRune(rb[:], lo)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up)
rb = shiftNRuneBytes(rb, off)
idxc := rb[0]
for i, c := range []byte(n.indices) {
if c == idxc {
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out
}
break
}
}
}
}
}
// Fall back to wildcard child, which is always at the end of the array
n = n.children[len(n.children)-1]
switch n.nType {
case param:
// Find param end (either '/' or path end)

View File

@ -481,7 +481,7 @@ func TestTreeDuplicatePath(t *testing.T) {
}
}
//printChildren(tree, "")
// printChildren(tree, "")
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
@ -532,7 +532,7 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{}
var route = "/cmd/*filepath"
route := "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route))
}
@ -692,7 +692,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
}
func TestRedirectTrailingSlash(t *testing.T) {
var data = []struct {
data := []struct {
path string
}{
{"/hello/:name"},
@ -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))
}
}

View File

@ -6,6 +6,7 @@ package gin
import (
"encoding/xml"
"math"
"net/http"
"os"
"path"
@ -18,6 +19,12 @@ import (
// BindKey indicates a default bind key.
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.
func Bind(val any) HandlerFunc {
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
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
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)
}

View File

@ -8,10 +8,12 @@ import (
"bytes"
"encoding/xml"
"fmt"
"math"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func init() {
@ -19,7 +21,7 @@ func init() {
}
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}
@ -94,7 +96,7 @@ func somefunction() {
}
func TestJoinPaths(t *testing.T) {
assert.Equal(t, "", joinPaths("", ""))
assert.Empty(t, joinPaths("", ""))
assert.Equal(t, "/", joinPaths("", "/"))
assert.Equal(t, "/a", joinPaths("/a", ""))
assert.Equal(t, "/a/", joinPaths("/a/", ""))
@ -144,7 +146,28 @@ func TestMarshalXMLforH(t *testing.T) {
assert.Error(t, e)
}
func TestMarshalXMLforHSuccess(t *testing.T) {
h := H{
"key1": "value1",
"key2": 123,
}
data, err := xml.Marshal(h)
require.NoError(t, err)
assert.Contains(t, string(data), "<key1>value1</key1>")
assert.Contains(t, string(data), "<key2>123</key2>")
}
func TestIsASCII(t *testing.T) {
assert.True(t, isASCII("test"))
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))
}

View File

@ -5,4 +5,4 @@
package gin
// Version is the current gin framework's version.
const Version = "v1.10.0"
const Version = "v1.12.0"