mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 09:48:17 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
d81699791a
49
.github/ISSUE_TEMPLATE.md
vendored
49
.github/ISSUE_TEMPLATE.md
vendored
@ -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
60
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
Normal 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
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||||
18
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
Normal 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
|
||||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +1,10 @@
|
|||||||
- With pull requests:
|
# Pull Request Checklist
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
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!
|
||||||
|
|||||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@ -1,10 +1,14 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -33,11 +33,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
20
.github/workflows/gin.yml
vendored
20
.github/workflows/gin.yml
vendored
@ -16,29 +16,29 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1"
|
go-version: "^1"
|
||||||
- name: Setup golangci-lint
|
- name: Setup golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v1.61.0
|
version: v2.9
|
||||||
args: --verbose
|
args: --verbose
|
||||||
test:
|
test:
|
||||||
needs: lint
|
needs: lint
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
go: ["1.23", "1.24"]
|
go: ["1.24", "1.25", "1.26"]
|
||||||
test-tags:
|
test-tags:
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"-tags nomsgpack",
|
"-tags nomsgpack",
|
||||||
'--ldflags="-checklinkname=0" -tags "sonic avx"',
|
'--ldflags="-checklinkname=0" -tags sonic',
|
||||||
"-tags go_json",
|
"-tags go_json",
|
||||||
"-race",
|
"-race",
|
||||||
]
|
]
|
||||||
@ -55,17 +55,17 @@ jobs:
|
|||||||
GOPROXY: https://proxy.golang.org
|
GOPROXY: https://proxy.golang.org
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ matrix.go-build }}
|
${{ matrix.go-build }}
|
||||||
@ -78,6 +78,6 @@ jobs:
|
|||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
||||||
|
|||||||
11
.github/workflows/goreleaser.yml
vendored
11
.github/workflows/goreleaser.yml
vendored
@ -13,15 +13,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1"
|
go-version: "^1"
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
@ -29,3 +29,8 @@ jobs:
|
|||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
56
.github/workflows/trivy-scan.yml
vendored
Normal 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"
|
||||||
114
.golangci.yml
114
.golangci.yml
@ -1,16 +1,11 @@
|
|||||||
run:
|
version: "2"
|
||||||
timeout: 5m
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- asciicheck
|
- asciicheck
|
||||||
|
- copyloopvar
|
||||||
- dogsled
|
- dogsled
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- errcheck
|
|
||||||
- errorlint
|
- errorlint
|
||||||
- copyloopvar
|
|
||||||
- gci
|
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
- gosec
|
- gosec
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
@ -21,51 +16,60 @@ linters:
|
|||||||
- testifylint
|
- testifylint
|
||||||
- usestdlibvars
|
- usestdlibvars
|
||||||
- wastedassign
|
- wastedassign
|
||||||
|
settings:
|
||||||
linters-settings:
|
gosec:
|
||||||
gosec:
|
excludes:
|
||||||
# To select a subset of rules to run.
|
- G115
|
||||||
# Available rules: https://github.com/securego/gosec#available-rules
|
perfsprint:
|
||||||
# Default: [] - means include all rules
|
int-conversion: true
|
||||||
includes:
|
err-error: true
|
||||||
- G102
|
errorf: true
|
||||||
- G106
|
sprintf1: true
|
||||||
- G108
|
strconcat: true
|
||||||
- G109
|
testifylint:
|
||||||
- G111
|
enable-all: true
|
||||||
- G112
|
exclusions:
|
||||||
- G201
|
generated: lax
|
||||||
- G203
|
presets:
|
||||||
perfsprint:
|
- comments
|
||||||
err-error: true
|
- common-false-positives
|
||||||
errorf: true
|
- legacy
|
||||||
int-conversion: true
|
- std-error-handling
|
||||||
sprintf1: true
|
rules:
|
||||||
strconcat: true
|
- linters:
|
||||||
testifylint:
|
- structcheck
|
||||||
enable-all: true
|
- unused
|
||||||
|
text: '`data` is unused'
|
||||||
issues:
|
- linters:
|
||||||
exclude-rules:
|
- staticcheck
|
||||||
- linters:
|
text: 'SA1019:'
|
||||||
- structcheck
|
- linters:
|
||||||
- unused
|
- revive
|
||||||
text: "`data` is unused"
|
text: 'var-naming:'
|
||||||
- linters:
|
- linters:
|
||||||
- staticcheck
|
- revive
|
||||||
text: "SA1019:"
|
text: 'exported:'
|
||||||
- linters:
|
- linters:
|
||||||
- revive
|
- gosec
|
||||||
text: "var-naming:"
|
path: _test\.go
|
||||||
- linters:
|
- linters:
|
||||||
- revive
|
- revive
|
||||||
text: "exported:"
|
path: _test\.go
|
||||||
- path: _test\.go
|
paths:
|
||||||
linters:
|
- third_party$
|
||||||
- gosec # security is not make sense in tests
|
- builtin$
|
||||||
- linters:
|
- examples$
|
||||||
- revive
|
formatters:
|
||||||
path: _test\.go
|
enable:
|
||||||
- path: gin.go
|
- gofmt
|
||||||
linters:
|
- gofumpt
|
||||||
- gci
|
- goimports
|
||||||
|
settings:
|
||||||
|
gofmt:
|
||||||
|
rewrite-rules:
|
||||||
|
- pattern: 'interface{}'
|
||||||
|
replacement: 'any'
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- gin.go
|
||||||
|
|||||||
96
CHANGELOG.md
96
CHANGELOG.md
@ -1,5 +1,99 @@
|
|||||||
# Gin ChangeLog
|
# Gin ChangeLog
|
||||||
|
|
||||||
|
## 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
|
## Gin v1.10.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@ -26,7 +120,7 @@
|
|||||||
* fix(uri): query binding bug (#3236) (@illiafox)
|
* fix(uri): query binding bug (#3236) (@illiafox)
|
||||||
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
||||||
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* chore(CI): update release args (#3595) (@qloog)
|
* chore(CI): update release args (#3595) (@qloog)
|
||||||
|
|||||||
@ -1,13 +1,41 @@
|
|||||||
## Contributing
|
# Contributing
|
||||||
|
|
||||||
- With issues:
|
We welcome both issue reports and pull requests! Please follow these guidelines to help maintainers respond effectively.
|
||||||
- Use the search tool before opening a new issue.
|
|
||||||
- Please provide source code and commit sha if you found a bug.
|
## 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.
|
- 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:
|
- **Reporting a bug:**
|
||||||
- Open your pull request against `master`
|
- Please provide a clear description of your issue, and a minimal reproducible code example if possible.
|
||||||
- Your pull request should have no more than two commits, if not you should squash them.
|
- Include the Gin version (or commit reference), Go version, and operating system.
|
||||||
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
|
- Indicate whether you can reproduce the bug and describe steps to do so.
|
||||||
- You should add/modify tests to cover your proposed code changes.
|
- Attach relevant logs per [Logging Documentation](https://docs.gitea.com/administration/logging-config#collecting-logs-for-help).
|
||||||
- If your pull request contains a new feature, please document it on the README.
|
|
||||||
|
- **Feature requests:**
|
||||||
|
- Before opening a request, check that a similar idea hasn’t 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!
|
||||||
|
|||||||
190
README.md
190
README.md
@ -2,116 +2,150 @@
|
|||||||
|
|
||||||
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
|
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
|
||||||
|
|
||||||
[](https://github.com/gin-gonic/gin/actions?query=branch%3Amaster)
|
[](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
|
||||||
|
[](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
|
||||||
[](https://codecov.io/gh/gin-gonic/gin)
|
[](https://codecov.io/gh/gin-gonic/gin)
|
||||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||||
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
||||||
[](https://www.codetriage.com/gin-gonic/gin)
|
[](https://www.codetriage.com/gin-gonic/gin)
|
||||||
[](https://github.com/gin-gonic/gin/releases)
|
[](https://github.com/gin-gonic/gin/releases)
|
||||||
[](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).
|
## 📰 [Announcing Gin 1.11.0!](https://gin-gonic.com/en/blog/news/gin-1-11-0-release-announcement/)
|
||||||
If you need performance and good productivity, you will love Gin.
|
|
||||||
|
|
||||||
**Gin's key features are:**
|
Read about the latest features and improvements in Gin 1.11.0 on our official blog.
|
||||||
|
|
||||||
- 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
|
### Prerequisites
|
||||||
|
|
||||||
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above.
|
- **Go version**: Gin requires [Go](https://go.dev/) version [1.24](https://go.dev/doc/devel/release#go1.24.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"
|
import "github.com/gin-gonic/gin"
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, use `go get`:
|
### Your First Gin Application
|
||||||
|
|
||||||
```sh
|
Here's a complete example that demonstrates Gin's simplicity:
|
||||||
go get -u github.com/gin-gonic/gin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Gin
|
|
||||||
|
|
||||||
A basic example:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Create a Gin router with default middleware (logger and recovery)
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
// Define a simple GET endpoint
|
||||||
r.GET("/ping", func(c *gin.Context) {
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
// Return JSON response
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "pong",
|
"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
|
1. Save the code above as `main.go`
|
||||||
go run example.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/)
|
### API Reference
|
||||||
- [简体中文](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/)
|
|
||||||
- [Russian](https://gin-gonic.com/ru/docs/)
|
|
||||||
|
|
||||||
### 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) |
|
| Benchmark name | (1) | (2) | (3) | (4) |
|
||||||
| ------------------------------ | --------: | --------------: | -----------: | --------------: |
|
| ------------------------------ | --------: | --------------: | -----------: | --------------: |
|
||||||
@ -151,23 +185,43 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
|
|||||||
- (3): Heap Memory (B/op), lower is better
|
- (3): Heap Memory (B/op), lower is better
|
||||||
- (4): Average Allocations per Repetition (allocs/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.
|
Gin powers many high-traffic applications and services in production:
|
||||||
- [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.
|
|
||||||
|
|
||||||
## 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!**
|
||||||
|
|||||||
@ -87,7 +87,7 @@ func BenchmarkOneRouteString(B *testing.B) {
|
|||||||
runRequest(B, router, http.MethodGet, "/text")
|
runRequest(B, router, http.MethodGet, "/text")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkManyRoutesFist(B *testing.B) {
|
func BenchmarkManyRoutesFirst(B *testing.B) {
|
||||||
router := New()
|
router := New()
|
||||||
router.Any("/ping", func(c *Context) {})
|
router.Any("/ping", func(c *Context) {})
|
||||||
runRequest(B, router, http.MethodGet, "/ping")
|
runRequest(B, router, http.MethodGet, "/ping")
|
||||||
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
|
|||||||
w := newMockWriter()
|
w := newMockWriter()
|
||||||
B.ReportAllocs()
|
B.ReportAllocs()
|
||||||
B.ResetTimer()
|
B.ResetTimer()
|
||||||
for i := 0; i < B.N; i++ {
|
for B.Loop() {
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const (
|
|||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
MIMEYAML2 = "application/yaml"
|
MIMEYAML2 = "application/yaml"
|
||||||
MIMETOML = "application/toml"
|
MIMETOML = "application/toml"
|
||||||
|
MIMEBSON = "application/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -86,6 +87,7 @@ var (
|
|||||||
Header Binding = headerBinding{}
|
Header Binding = headerBinding{}
|
||||||
Plain BindingBody = plainBinding{}
|
Plain BindingBody = plainBinding{}
|
||||||
TOML BindingBody = tomlBinding{}
|
TOML BindingBody = tomlBinding{}
|
||||||
|
BSON BindingBody = bsonBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
|
|||||||
return TOML
|
return TOML
|
||||||
case MIMEMultipartPOSTForm:
|
case MIMEMultipartPOSTForm:
|
||||||
return FormMultipart
|
return FormMultipart
|
||||||
|
case MIMEBSON:
|
||||||
|
return BSON
|
||||||
default: // case MIMEPOSTForm:
|
default: // case MIMEPOSTForm:
|
||||||
return Form
|
return Form
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const (
|
|||||||
MIMEYAML = "application/x-yaml"
|
MIMEYAML = "application/x-yaml"
|
||||||
MIMEYAML2 = "application/yaml"
|
MIMEYAML2 = "application/yaml"
|
||||||
MIMETOML = "application/toml"
|
MIMETOML = "application/toml"
|
||||||
|
MIMEBSON = "application/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -82,6 +83,7 @@ var (
|
|||||||
Header = headerBinding{}
|
Header = headerBinding{}
|
||||||
TOML = tomlBinding{}
|
TOML = tomlBinding{}
|
||||||
Plain = plainBinding{}
|
Plain = plainBinding{}
|
||||||
|
BSON BindingBody = bsonBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
|
|||||||
return FormMultipart
|
return FormMultipart
|
||||||
case MIMETOML:
|
case MIMETOML:
|
||||||
return TOML
|
return TOML
|
||||||
|
case MIMEBSON:
|
||||||
|
return BSON
|
||||||
default: // case MIMEPOSTForm:
|
default: // case MIMEPOSTForm:
|
||||||
return Form
|
return Form
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,8 +52,6 @@ type FooBarFileStruct struct {
|
|||||||
type FooBarFileFailStruct struct {
|
type FooBarFileFailStruct struct {
|
||||||
FooBarStruct
|
FooBarStruct
|
||||||
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
|
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
|
||||||
// for unexport test
|
|
||||||
data *multipart.FileHeader `form:"data" binding:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooDefaultBarStruct struct {
|
type FooDefaultBarStruct struct {
|
||||||
@ -174,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
|
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
|
||||||
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
|
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
|
||||||
|
|
||||||
|
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
|
||||||
|
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSONNilBody(t *testing.T) {
|
func TestBindingJSONNilBody(t *testing.T) {
|
||||||
@ -733,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
|
|||||||
string(data), string(data[1:]))
|
string(data), string(data[1:]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingBSON(t *testing.T) {
|
||||||
|
var obj FooStruct
|
||||||
|
obj.Foo = "bar"
|
||||||
|
data, _ := bson.Marshal(&obj)
|
||||||
|
testBodyBinding(t,
|
||||||
|
BSON, "bson",
|
||||||
|
"/", "/",
|
||||||
|
string(data),
|
||||||
|
// note: for badbody, we remove first byte to make it invalid
|
||||||
|
string(data[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidationFails(t *testing.T) {
|
func TestValidationFails(t *testing.T) {
|
||||||
var obj FooStruct
|
var obj FooStruct
|
||||||
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
|
||||||
@ -1063,7 +1077,7 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
|
|||||||
}
|
}
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "", obj.TestName)
|
assert.Empty(t, obj.TestName)
|
||||||
|
|
||||||
obj = InvalidNameType{}
|
obj = InvalidNameType{}
|
||||||
req = requestWithBody(method, badPath, badBody)
|
req = requestWithBody(method, badPath, badBody)
|
||||||
@ -1318,7 +1332,7 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
|
|||||||
req := requestWithBody(http.MethodPost, path, body)
|
req := requestWithBody(http.MethodPost, path, body)
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Equal(t, "", obj.Foo)
|
assert.Empty(t, obj.Foo)
|
||||||
|
|
||||||
obj = FooStruct{}
|
obj = FooStruct{}
|
||||||
req = requestWithBody(http.MethodPost, badPath, badBody)
|
req = requestWithBody(http.MethodPost, badPath, badBody)
|
||||||
|
|||||||
30
binding/bson.go
Normal file
30
binding/bson.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bsonBinding struct{}
|
||||||
|
|
||||||
|
func (bsonBinding) Name() string {
|
||||||
|
return "bson"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bsonBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
buf, err := io.ReadAll(req.Body)
|
||||||
|
if err == nil {
|
||||||
|
err = b.BindBody(buf, obj)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bsonBinding) BindBody(body []byte, obj any) error {
|
||||||
|
return bson.Unmarshal(body, obj)
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := 0; i < len(err); i++ {
|
for i := range len(err) {
|
||||||
if err[i] != nil {
|
if err[i] != nil {
|
||||||
if b.Len() > 0 {
|
if b.Len() > 0 {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
|||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
count := value.Len()
|
count := value.Len()
|
||||||
validateRet := make(SliceValidationError, 0)
|
validateRet := make(SliceValidationError, 0)
|
||||||
for i := 0; i < count; i++ {
|
for i := range count {
|
||||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||||
validateRet = append(validateRet, err)
|
validateRet = append(validateRet, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,9 +18,8 @@ func BenchmarkSliceValidationError(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
if len(e.Error()) == 0 {
|
if len(e.Error()) == 0 {
|
||||||
b.Errorf("error")
|
b.Errorf("error")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,14 +18,16 @@ func TestSliceValidationError(t *testing.T) {
|
|||||||
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
|
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
|
||||||
{"has zero elements", SliceValidationError{}, ""},
|
{"has zero elements", SliceValidationError{}, ""},
|
||||||
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
||||||
{"has two elements",
|
{
|
||||||
|
"has two elements",
|
||||||
SliceValidationError{
|
SliceValidationError{
|
||||||
errors.New("first error"),
|
errors.New("first error"),
|
||||||
errors.New("second error"),
|
errors.New("second error"),
|
||||||
},
|
},
|
||||||
"[0]: first error\n[1]: second error",
|
"[0]: first error\n[1]: second error",
|
||||||
},
|
},
|
||||||
{"has many elements",
|
{
|
||||||
|
"has many elements",
|
||||||
SliceValidationError{
|
SliceValidationError{
|
||||||
errors.New("first error"),
|
errors.New("first error"),
|
||||||
errors.New("second error"),
|
errors.New("second error"),
|
||||||
|
|||||||
@ -11,9 +11,11 @@ import (
|
|||||||
|
|
||||||
const defaultMemory = 32 << 20
|
const defaultMemory = 32 << 20
|
||||||
|
|
||||||
type formBinding struct{}
|
type (
|
||||||
type formPostBinding struct{}
|
formBinding struct{}
|
||||||
type formMultipartBinding struct{}
|
formPostBinding struct{}
|
||||||
|
formMultipartBinding struct{}
|
||||||
|
)
|
||||||
|
|
||||||
func (formBinding) Name() string {
|
func (formBinding) Name() string {
|
||||||
return "form"
|
return "form"
|
||||||
|
|||||||
@ -5,16 +5,18 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -117,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
tValue := value.Type()
|
tValue := value.Type()
|
||||||
|
|
||||||
var isSet bool
|
var isSet bool
|
||||||
for i := 0; i < value.NumField(); i++ {
|
for i := range value.NumField() {
|
||||||
sf := tValue.Field(i)
|
sf := tValue.Field(i)
|
||||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||||
continue
|
continue
|
||||||
@ -136,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
type setOptions struct {
|
type setOptions struct {
|
||||||
isDefaultExists bool
|
isDefaultExists bool
|
||||||
defaultValue string
|
defaultValue string
|
||||||
|
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
|
||||||
|
parser string
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
||||||
@ -167,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if k, v = head(opt, "="); k == "parser" {
|
||||||
|
setOpt.parser = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +181,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
|
|
||||||
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||||
type BindUnmarshaler interface {
|
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
|
UnmarshalParam(param string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
|
||||||
|
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
|
||||||
|
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
|
||||||
|
switch parser {
|
||||||
|
case "encoding.TextUnmarshaler":
|
||||||
|
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, v.UnmarshalText([]byte(val))
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
||||||
cfTag := field.Tag.Get("collection_format")
|
cfTag := field.Tag.Get("collection_format")
|
||||||
if cfTag == "" || cfTag == "multi" {
|
if cfTag == "" || cfTag == "multi" {
|
||||||
@ -207,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
|
|||||||
case "pipes":
|
case "pipes":
|
||||||
sep = "|"
|
sep = "|"
|
||||||
default:
|
default:
|
||||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
|
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalLength := 0
|
totalLength := 0
|
||||||
@ -230,9 +250,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
|
|
||||||
switch value.Kind() {
|
switch value.Kind() {
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if !ok {
|
if len(vs) == 0 {
|
||||||
vs = []string{opt.defaultValue}
|
if !opt.isDefaultExists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vs = []string{opt.defaultValue}
|
||||||
// pre-process the default value for multi if present
|
// pre-process the default value for multi if present
|
||||||
cfTag := field.Tag.Get("collection_format")
|
cfTag := field.Tag.Get("collection_format")
|
||||||
if cfTag == "" || cfTag == "multi" {
|
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
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,11 +273,14 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setSlice(vs, value, field)
|
return true, setSlice(vs, value, field, opt)
|
||||||
case reflect.Array:
|
case reflect.Array:
|
||||||
if !ok {
|
if len(vs) == 0 {
|
||||||
vs = []string{opt.defaultValue}
|
if !opt.isDefaultExists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vs = []string{opt.defaultValue}
|
||||||
// pre-process the default value for multi if present
|
// pre-process the default value for multi if present
|
||||||
cfTag := field.Tag.Get("collection_format")
|
cfTag := field.Tag.Get("collection_format")
|
||||||
if cfTag == "" || cfTag == "multi" {
|
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
|
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 false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setArray(vs, value, field)
|
return true, setArray(vs, value, field, opt)
|
||||||
default:
|
default:
|
||||||
var val string
|
var val string
|
||||||
if !ok {
|
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||||
val = opt.defaultValue
|
val = opt.defaultValue
|
||||||
|
} else if len(vs) > 0 {
|
||||||
|
val = vs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(vs) > 0 {
|
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||||
val = vs[0]
|
return ok, err
|
||||||
if val == "" {
|
} else if ok, err = trySetCustom(val, value); ok {
|
||||||
val = opt.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok, err := trySetCustom(val, value); ok {
|
|
||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
return true, setWithProperType(val, value, field)
|
return true, setWithProperType(val, value, field, opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
|
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
|
||||||
|
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
|
||||||
|
return err
|
||||||
|
} else if ok, err = trySetCustom(val, value); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||||
|
if value.Kind() != reflect.String {
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
}
|
||||||
|
|
||||||
switch value.Kind() {
|
switch value.Kind() {
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
return setIntField(val, 0, value)
|
return setIntField(val, 0, value)
|
||||||
@ -333,14 +373,14 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||||||
case multipart.FileHeader:
|
case multipart.FileHeader:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||||
case reflect.Map:
|
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:
|
case reflect.Ptr:
|
||||||
if !value.Elem().IsValid() {
|
if !value.Elem().IsValid() {
|
||||||
value.Set(reflect.New(value.Type().Elem()))
|
value.Set(reflect.New(value.Type().Elem()))
|
||||||
}
|
}
|
||||||
return setWithProperType(val, value.Elem(), field)
|
return setWithProperType(val, value.Elem(), field, opt)
|
||||||
default:
|
default:
|
||||||
return errUnknownType
|
return errUnknownType
|
||||||
}
|
}
|
||||||
@ -397,6 +437,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
timeFormat = time.RFC3339
|
timeFormat = time.RFC3339
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
value.Set(reflect.ValueOf(time.Time{}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
switch tf := strings.ToLower(timeFormat); tf {
|
switch tf := strings.ToLower(timeFormat); tf {
|
||||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||||
tv, err := strconv.ParseInt(val, 10, 64)
|
tv, err := strconv.ParseInt(val, 10, 64)
|
||||||
@ -420,11 +465,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
value.Set(reflect.ValueOf(time.Time{}))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l := time.Local
|
l := time.Local
|
||||||
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
||||||
l = time.UTC
|
l = time.UTC
|
||||||
@ -447,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
|
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
for i, s := range vals {
|
for i, s := range vals {
|
||||||
err := setWithProperType(s, value.Index(i), field)
|
err := setWithProperType(s, value.Index(i), field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -457,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
|
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||||
err := setArray(vals, slice, field)
|
err := setArray(vals, slice, field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -468,6 +508,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setTimeDuration(val string, value reflect.Value) error {
|
func setTimeDuration(val string, value reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "0"
|
||||||
|
}
|
||||||
|
|
||||||
d, err := time.ParseDuration(val)
|
d, err := time.ParseDuration(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -489,9 +533,7 @@ func setFormMap(ptr any, form map[string][]string) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ErrConvertMapStringSlice
|
return ErrConvertMapStringSlice
|
||||||
}
|
}
|
||||||
for k, v := range form {
|
maps.Copy(ptrMap, form)
|
||||||
ptrMap[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ type structFull struct {
|
|||||||
|
|
||||||
func BenchmarkMapFormFull(b *testing.B) {
|
func BenchmarkMapFormFull(b *testing.B) {
|
||||||
var s structFull
|
var s structFull
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
err := mapForm(&s, form)
|
err := mapForm(&s, form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Error on a form mapping")
|
b.Fatalf("Error on a form mapping")
|
||||||
@ -54,7 +54,7 @@ type structName struct {
|
|||||||
|
|
||||||
func BenchmarkMapFormName(b *testing.B) {
|
func BenchmarkMapFormName(b *testing.B) {
|
||||||
var s structName
|
var s structName
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
err := mapForm(&s, form)
|
err := mapForm(&s, form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Error on a form mapping")
|
b.Fatalf("Error on a form mapping")
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
@ -226,7 +227,35 @@ func TestMappingTime(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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) {
|
func TestMappingTimeDuration(t *testing.T) {
|
||||||
|
type needFixDurationEmpty struct {
|
||||||
|
Duration time.Duration `form:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
var s struct {
|
var s struct {
|
||||||
D time.Duration
|
D time.Duration
|
||||||
}
|
}
|
||||||
@ -236,6 +265,17 @@ func TestMappingTimeDuration(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 5*time.Second, s.D)
|
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
|
// error
|
||||||
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@ -485,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
|||||||
assert.EqualValues(t, 245, s.Foo)
|
assert.EqualValues(t, 245, s.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo customUnmarshalParamHex `form:"foo,default=f5"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"foo": {}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 0xf5, s.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
type customUnmarshalParamType struct {
|
type customUnmarshalParamType struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
Path string
|
Path string
|
||||||
@ -509,9 +559,9 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "file", s.FileData.Protocol)
|
assert.Equal(t, "file", s.FileData.Protocol)
|
||||||
assert.EqualValues(t, "/foo", s.FileData.Path)
|
assert.Equal(t, "/foo", s.FileData.Path)
|
||||||
assert.EqualValues(t, "happiness", s.FileData.Name)
|
assert.Equal(t, "happiness", s.FileData.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||||
@ -521,9 +571,9 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "file", s.FileData.Protocol)
|
assert.Equal(t, "file", s.FileData.Protocol)
|
||||||
assert.EqualValues(t, "/foo", s.FileData.Path)
|
assert.Equal(t, "/foo", s.FileData.Path)
|
||||||
assert.EqualValues(t, "happiness", s.FileData.Name)
|
assert.Equal(t, "happiness", s.FileData.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||||
@ -533,9 +583,9 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "file", s.FileData.Protocol)
|
assert.Equal(t, "file", s.FileData.Protocol)
|
||||||
assert.EqualValues(t, "/foo", s.FileData.Path)
|
assert.Equal(t, "/foo", s.FileData.Path)
|
||||||
assert.EqualValues(t, "happiness", s.FileData.Name)
|
assert.Equal(t, "happiness", s.FileData.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||||
@ -545,9 +595,9 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "file", s.FileData.Protocol)
|
assert.Equal(t, "file", s.FileData.Protocol)
|
||||||
assert.EqualValues(t, "/foo", s.FileData.Path)
|
assert.Equal(t, "/foo", s.FileData.Path)
|
||||||
assert.EqualValues(t, "happiness", s.FileData.Name)
|
assert.Equal(t, "happiness", s.FileData.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type customPath []string
|
type customPath []string
|
||||||
@ -570,8 +620,8 @@ func TestMappingCustomSliceUri(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
|
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "bar", s.FileData[0])
|
assert.Equal(t, "bar", s.FileData[0])
|
||||||
assert.EqualValues(t, "foo", s.FileData[1])
|
assert.Equal(t, "foo", s.FileData[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingCustomSliceForm(t *testing.T) {
|
func TestMappingCustomSliceForm(t *testing.T) {
|
||||||
@ -581,8 +631,35 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
|
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, "bar", s.FileData[0])
|
assert.Equal(t, "bar", s.FileData[0])
|
||||||
assert.EqualValues(t, "foo", s.FileData[1])
|
assert.Equal(t, "foo", s.FileData[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customPath `form:"path"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
|
||||||
|
require.ErrorContains(t, err, "invalid format")
|
||||||
|
require.Empty(t, s.FileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomSliceOfSliceUri(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData []customPath `uri:"path" collection_format:"csv"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomSliceOfSliceForm(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData []customPath `form:"path" collection_format:"csv"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||||
}
|
}
|
||||||
|
|
||||||
type objectID [12]byte
|
type objectID [12]byte
|
||||||
@ -621,7 +698,7 @@ func TestMappingCustomArrayUri(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected, _ := convertTo(val)
|
expected, _ := convertTo(val)
|
||||||
assert.EqualValues(t, expected, s.FileData)
|
assert.Equal(t, expected, s.FileData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappingCustomArrayForm(t *testing.T) {
|
func TestMappingCustomArrayForm(t *testing.T) {
|
||||||
@ -633,5 +710,437 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected, _ := convertTo(val)
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ func (headerBinding) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (headerBinding) Bind(req *http.Request, obj any) error {
|
func (headerBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
|
||||||
if err := mapHeader(obj, req.Header); err != nil {
|
if err := mapHeader(obj, req.Header); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"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
|
// 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 {
|
func decodeJSON(r io.Reader, obj any) error {
|
||||||
decoder := json.NewDecoder(r)
|
decoder := json.API.NewDecoder(r)
|
||||||
if EnableDecoderUseNumber {
|
if EnableDecoderUseNumber {
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,16 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
|
|||||||
assert.Equal(t, "FOO", s["foo"])
|
assert.Equal(t, "FOO", s["foo"])
|
||||||
assert.Equal(t, "world", s["hello"])
|
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
|
||||||
|
|||||||
@ -15,7 +15,7 @@ func (plainBinding) Name() string {
|
|||||||
return "plain"
|
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)
|
all, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -158,16 +158,16 @@ type structNoValidationPointer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateNoValidationPointers(t *testing.T) {
|
func TestValidateNoValidationPointers(t *testing.T) {
|
||||||
//origin := createNoValidation_values()
|
// origin := createNoValidation_values()
|
||||||
//test := createNoValidation_values()
|
// test := createNoValidation_values()
|
||||||
empty := structNoValidationPointer{}
|
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))
|
||||||
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
|
type Object map[string]any
|
||||||
@ -198,7 +198,7 @@ type structModifyValidation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toZero(sl validator.StructLevel) {
|
func toZero(sl validator.StructLevel) {
|
||||||
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
|
s := sl.Top().Interface().(*structModifyValidation)
|
||||||
s.Integer = 0
|
s.Integer = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,5 +249,5 @@ func TestValidatorEngine(t *testing.T) {
|
|||||||
// Check that we got back non-nil errs
|
// Check that we got back non-nil errs
|
||||||
require.Error(t, errs)
|
require.Error(t, errs)
|
||||||
// Check that the error matches expectation
|
// Check that the error matches expectation
|
||||||
require.Error(t, errs, "", "", "notone")
|
require.Error(t, errs, "notone")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ func (xmlBinding) Bind(req *http.Request, obj any) error {
|
|||||||
func (xmlBinding) BindBody(body []byte, obj any) error {
|
func (xmlBinding) BindBody(body []byte, obj any) error {
|
||||||
return decodeXML(bytes.NewReader(body), obj)
|
return decodeXML(bytes.NewReader(body), obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeXML(r io.Reader, obj any) error {
|
func decodeXML(r io.Reader, obj any) error {
|
||||||
decoder := xml.NewDecoder(r)
|
decoder := xml.NewDecoder(r)
|
||||||
if err := decoder.Decode(obj); err != nil {
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"github.com/goccy/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type yamlBinding struct{}
|
type yamlBinding struct{}
|
||||||
|
|||||||
57
codec/json/api.go
Normal file
57
codec/json/api.go
Normal 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
42
codec/json/go_json.go
Normal 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
41
codec/json/json.go
Normal 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
44
codec/json/jsoniter.go
Normal 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
44
codec/json/sonic.go
Normal 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)
|
||||||
|
}
|
||||||
282
context.go
282
context.go
@ -6,9 +6,11 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
@ -37,6 +39,8 @@ const (
|
|||||||
MIMEYAML = binding.MIMEYAML
|
MIMEYAML = binding.MIMEYAML
|
||||||
MIMEYAML2 = binding.MIMEYAML2
|
MIMEYAML2 = binding.MIMEYAML2
|
||||||
MIMETOML = binding.MIMETOML
|
MIMETOML = binding.MIMETOML
|
||||||
|
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||||
|
MIMEBSON = binding.MIMEBSON
|
||||||
)
|
)
|
||||||
|
|
||||||
// BodyBytesKey indicates a default body bytes key.
|
// BodyBytesKey indicates a default body bytes key.
|
||||||
@ -72,7 +76,7 @@ type Context struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
// Keys is a key/value pair exclusively for the context of each request.
|
// 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 is a list of errors attached to all the handlers/middlewares who used this context.
|
||||||
Errors errorMsgs
|
Errors errorMsgs
|
||||||
@ -129,11 +133,8 @@ func (c *Context) Copy() *Context {
|
|||||||
cp.fullPath = c.fullPath
|
cp.fullPath = c.fullPath
|
||||||
|
|
||||||
cKeys := c.Keys
|
cKeys := c.Keys
|
||||||
cp.Keys = make(map[string]any, len(cKeys))
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
for k, v := range cKeys {
|
cp.Keys = maps.Clone(cKeys)
|
||||||
cp.Keys[k] = v
|
|
||||||
}
|
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
cParams := c.Params
|
cParams := c.Params
|
||||||
@ -186,7 +187,7 @@ func (c *Context) FullPath() string {
|
|||||||
// See example in GitHub.
|
// See example in GitHub.
|
||||||
func (c *Context) Next() {
|
func (c *Context) Next() {
|
||||||
c.index++
|
c.index++
|
||||||
for c.index < int8(len(c.handlers)) {
|
for c.index < safeInt8(len(c.handlers)) {
|
||||||
if c.handlers[c.index] != nil {
|
if c.handlers[c.index] != nil {
|
||||||
c.handlers[c.index](c)
|
c.handlers[c.index](c)
|
||||||
}
|
}
|
||||||
@ -215,6 +216,14 @@ func (c *Context) AbortWithStatus(code int) {
|
|||||||
c.Abort()
|
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.
|
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
|
||||||
// This method stops the chain, writes the status code and return a JSON body.
|
// This method stops the chain, writes the status code and return a JSON body.
|
||||||
// It also sets the Content-Type as "application/json".
|
// It also sets the Content-Type as "application/json".
|
||||||
@ -263,12 +272,12 @@ func (c *Context) Error(err error) *Error {
|
|||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
// Set is used to store a new key/value pair exclusively for this context.
|
// 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.
|
// 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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if c.Keys == nil {
|
if c.Keys == nil {
|
||||||
c.Keys = make(map[string]any)
|
c.Keys = make(map[any]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Keys[key] = value
|
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).
|
// Get returns the value for the given key, ie: (value, true).
|
||||||
// If the value does not exist it returns (nil, false)
|
// 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()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
value, exists = c.Keys[key]
|
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.
|
// 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 {
|
if value, exists := c.Get(key); exists {
|
||||||
return value
|
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 {
|
if val, ok := c.Get(key); ok && val != nil {
|
||||||
res, _ = val.(T)
|
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.
|
// 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)
|
return getTyped[string](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBool returns the value associated with the key as a boolean.
|
// 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)
|
return getTyped[bool](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt returns the value associated with the key as an integer.
|
// 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)
|
return getTyped[int](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt8 returns the value associated with the key as an integer 8.
|
// 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)
|
return getTyped[int8](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt16 returns the value associated with the key as an integer 16.
|
// 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)
|
return getTyped[int16](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt32 returns the value associated with the key as an integer 32.
|
// 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)
|
return getTyped[int32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt64 returns the value associated with the key as an integer 64.
|
// 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)
|
return getTyped[int64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint returns the value associated with the key as an unsigned integer.
|
// 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)
|
return getTyped[uint](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint8 returns the value associated with the key as an unsigned integer 8.
|
// 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)
|
return getTyped[uint8](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint16 returns the value associated with the key as an unsigned integer 16.
|
// 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)
|
return getTyped[uint16](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint32 returns the value associated with the key as an unsigned integer 32.
|
// 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)
|
return getTyped[uint32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint64 returns the value associated with the key as an unsigned integer 64.
|
// 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)
|
return getTyped[uint64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat32 returns the value associated with the key as a float32.
|
// 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)
|
return getTyped[float32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat64 returns the value associated with the key as a float64.
|
// 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)
|
return getTyped[float64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTime returns the value associated with the key as time.
|
// 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)
|
return getTyped[time.Time](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDuration returns the value associated with the key as a duration.
|
// 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)
|
return getTyped[time.Duration](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetError returns the value associated with the key as an error.
|
||||||
|
func (c *Context) GetError(key any) error {
|
||||||
|
return getTyped[error](c, key)
|
||||||
|
}
|
||||||
|
|
||||||
// GetIntSlice returns the value associated with the key as a slice of integers.
|
// GetIntSlice returns the value associated with the key as a slice of integers.
|
||||||
func (c *Context) GetIntSlice(key string) (is []int) {
|
func (c *Context) GetIntSlice(key any) []int {
|
||||||
return getTyped[[]int](c, key)
|
return getTyped[[]int](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
|
// 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)
|
return getTyped[[]int8](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
|
// 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)
|
return getTyped[[]int16](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
|
// 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)
|
return getTyped[[]int32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
|
// 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)
|
return getTyped[[]int64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
|
// 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)
|
return getTyped[[]uint](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
|
// 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)
|
return getTyped[[]uint8](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
|
// 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)
|
return getTyped[[]uint16](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
|
// 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)
|
return getTyped[[]uint32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
|
// 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)
|
return getTyped[[]uint64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
|
// 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)
|
return getTyped[[]float32](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
|
// 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)
|
return getTyped[[]float64](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStringSlice returns the value associated with the key as a slice of strings.
|
// 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)
|
return getTyped[[]string](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetErrorSlice returns the value associated with the key as a slice of errors.
|
||||||
|
func (c *Context) GetErrorSlice(key any) []error {
|
||||||
|
return getTyped[[]error](c, key)
|
||||||
|
}
|
||||||
|
|
||||||
// GetStringMap returns the value associated with the key as a map of interfaces.
|
// GetStringMap returns the value associated with the key as a map of interfaces.
|
||||||
func (c *Context) GetStringMap(key string) (sm map[string]any) {
|
func (c *Context) GetStringMap(key any) map[string]any {
|
||||||
return getTyped[map[string]any](c, key)
|
return getTyped[map[string]any](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStringMapString returns the value associated with the key as a map of strings.
|
// 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)
|
return getTyped[map[string]string](c, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
|
// 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)
|
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 ************/
|
/************ 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.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
||||||
c.initQueryCache()
|
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
|
// 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.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
||||||
c.initFormCache()
|
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.
|
// getMapFromFormData return a map which satisfies conditions.
|
||||||
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
|
// It parses from data with bracket notation like "key[subkey]=value" into a map.
|
||||||
dicts := make(map[string]string)
|
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
|
||||||
exist := false
|
d := make(map[string]string)
|
||||||
|
found := false
|
||||||
|
keyLen := len(key)
|
||||||
|
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
|
if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
|
||||||
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
|
continue
|
||||||
exist = true
|
}
|
||||||
dicts[k[i+1:][:j]] = v[0]
|
|
||||||
}
|
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.
|
// FormFile returns the first file for the provided form key.
|
||||||
@ -712,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
|
|||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML binding
|
// "application/xml" --> XML binding
|
||||||
//
|
//
|
||||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||||
// It decodes the json payload into the struct specified as a pointer.
|
// It decodes the payload into the struct specified as a pointer.
|
||||||
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
||||||
func (c *Context) Bind(obj any) error {
|
func (c *Context) Bind(obj any) error {
|
||||||
b := binding.Default(c.Request.Method, c.ContentType())
|
b := binding.Default(c.Request.Method, c.ContentType())
|
||||||
@ -769,8 +808,19 @@ func (c *Context) BindUri(obj any) error {
|
|||||||
// It will abort the request with HTTP 400 if any error occurs.
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
// See the binding package.
|
// See the binding package.
|
||||||
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
||||||
if err := c.ShouldBindWith(obj, b); err != nil {
|
err := c.ShouldBindWith(obj, b)
|
||||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
|
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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -782,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
|||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML binding
|
// "application/xml" --> XML binding
|
||||||
//
|
//
|
||||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||||
// It decodes the json payload into the struct specified as a pointer.
|
// It decodes the payload into the struct specified as a pointer.
|
||||||
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
|
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
|
||||||
func (c *Context) ShouldBind(obj any) error {
|
func (c *Context) ShouldBind(obj any) error {
|
||||||
b := binding.Default(c.Request.Method, c.ContentType())
|
b := binding.Default(c.Request.Method, c.ContentType())
|
||||||
@ -791,41 +841,71 @@ func (c *Context) ShouldBind(obj any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
|
// 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 {
|
func (c *Context) ShouldBindJSON(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.JSON)
|
return c.ShouldBindWith(obj, binding.JSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
|
// 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 {
|
func (c *Context) ShouldBindXML(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.XML)
|
return c.ShouldBindWith(obj, binding.XML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
|
// 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 {
|
func (c *Context) ShouldBindQuery(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.Query)
|
return c.ShouldBindWith(obj, binding.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
|
// 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 {
|
func (c *Context) ShouldBindYAML(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.YAML)
|
return c.ShouldBindWith(obj, binding.YAML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
|
// 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 {
|
func (c *Context) ShouldBindTOML(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.TOML)
|
return c.ShouldBindWith(obj, binding.TOML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
|
// 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 {
|
func (c *Context) ShouldBindPlain(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.Plain)
|
return c.ShouldBindWith(obj, binding.Plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
// 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 {
|
func (c *Context) ShouldBindHeader(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.Header)
|
return c.ShouldBindWith(obj, binding.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
// 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 {
|
func (c *Context) ShouldBindUri(obj any) error {
|
||||||
m := make(map[string][]string, len(c.Params))
|
m := make(map[string][]string, len(c.Params))
|
||||||
for _, v := range c.Params {
|
for _, v := range c.Params {
|
||||||
@ -909,18 +989,32 @@ func (c *Context) ClientIP() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// It also checks if the remoteIP is a trusted proxy or not.
|
var (
|
||||||
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
trusted bool
|
||||||
// defined by Engine.SetTrustedProxies()
|
remoteIP net.IP
|
||||||
remoteIP := net.ParseIP(c.RemoteIP())
|
)
|
||||||
if remoteIP == nil {
|
// If gin is listening a unix socket, always trust it.
|
||||||
return ""
|
localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||||
|
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
|
||||||
|
trusted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
if !trusted {
|
||||||
|
// It also checks if the remoteIP is a trusted proxy or not.
|
||||||
|
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
||||||
|
// defined by Engine.SetTrustedProxies()
|
||||||
|
remoteIP = net.ParseIP(c.RemoteIP())
|
||||||
|
if remoteIP == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
trusted = c.engine.isTrustedProxy(remoteIP)
|
||||||
}
|
}
|
||||||
trusted := c.engine.isTrustedProxy(remoteIP)
|
|
||||||
|
|
||||||
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||||
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
|
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
|
||||||
|
ip, valid := c.engine.validateHeader(headerValue)
|
||||||
if valid {
|
if valid {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
@ -964,7 +1058,7 @@ func (c *Context) requestHeader(key string) string {
|
|||||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
||||||
func bodyAllowedForStatus(status int) bool {
|
func bodyAllowedForStatus(status int) bool {
|
||||||
switch {
|
switch {
|
||||||
case status >= 100 && status <= 199:
|
case status >= http.StatusContinue && status < http.StatusOK:
|
||||||
return false
|
return false
|
||||||
case status == http.StatusNoContent:
|
case status == http.StatusNoContent:
|
||||||
return false
|
return false
|
||||||
@ -1027,6 +1121,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
|
// Cookie returns the named cookie provided in the request or
|
||||||
// ErrNoCookie if not found. And return the named cookie is unescaped.
|
// ErrNoCookie if not found. And return the named cookie is unescaped.
|
||||||
// If multiple cookies match the given name, only one cookie will
|
// If multiple cookies match the given name, only one cookie will
|
||||||
@ -1131,6 +1238,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
|
|||||||
c.Render(code, render.ProtoBuf{Data: obj})
|
c.Render(code, render.ProtoBuf{Data: obj})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BSON serializes the given struct as BSON into the response body.
|
||||||
|
func (c *Context) BSON(code int, obj any) {
|
||||||
|
c.Render(code, render.BSON{Data: obj})
|
||||||
|
}
|
||||||
|
|
||||||
// String writes the given string into the response body.
|
// String writes the given string into the response body.
|
||||||
func (c *Context) String(code int, format string, values ...any) {
|
func (c *Context) String(code int, format string, values ...any) {
|
||||||
c.Render(code, render.String{Format: format, Data: values})
|
c.Render(code, render.String{Format: format, Data: values})
|
||||||
@ -1229,14 +1341,16 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
|
|||||||
|
|
||||||
// Negotiate contains all negotiations data.
|
// Negotiate contains all negotiations data.
|
||||||
type Negotiate struct {
|
type Negotiate struct {
|
||||||
Offered []string
|
Offered []string
|
||||||
HTMLName string
|
HTMLName string
|
||||||
HTMLData any
|
HTMLData any
|
||||||
JSONData any
|
JSONData any
|
||||||
XMLData any
|
XMLData any
|
||||||
YAMLData any
|
YAMLData any
|
||||||
Data any
|
Data any
|
||||||
TOMLData any
|
TOMLData any
|
||||||
|
PROTOBUFData any
|
||||||
|
BSONData any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negotiate calls different Render according to acceptable Accept format.
|
// Negotiate calls different Render according to acceptable Accept format.
|
||||||
@ -1262,6 +1376,14 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
|||||||
data := chooseData(config.TOMLData, config.Data)
|
data := chooseData(config.TOMLData, config.Data)
|
||||||
c.TOML(code, 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:
|
default:
|
||||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||||
}
|
}
|
||||||
|
|||||||
35
context_file_test.go
Normal file
35
context_file_test.go
Normal 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)
|
||||||
|
}
|
||||||
729
context_test.go
729
context_test.go
File diff suppressed because it is too large
Load Diff
10
debug.go
10
debug.go
@ -13,7 +13,9 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 21
|
const ginSupportMinGoVer = 24
|
||||||
|
|
||||||
|
var runtimeVersion = runtime.Version()
|
||||||
|
|
||||||
// IsDebugging returns true if the framework is running in debug mode.
|
// IsDebugging returns true if the framework is running in debug mode.
|
||||||
// Use SetMode(gin.ReleaseMode) to disable 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)
|
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
|
||||||
|
|
||||||
// DebugPrintFunc indicates debug log output format.
|
// 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) {
|
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
@ -77,8 +79,8 @@ func getMinVer(v string) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func debugPrintWARNINGDefault() {
|
func debugPrintWARNINGDefault() {
|
||||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
|
||||||
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
|
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@ -21,10 +20,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO
|
|
||||||
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
|
||||||
// func debugPrint(format string, values ...any) {
|
|
||||||
|
|
||||||
func TestIsDebugging(t *testing.T) {
|
func TestIsDebugging(t *testing.T) {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
assert.True(t, IsDebugging())
|
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)
|
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) {
|
func TestDebugPrintError(t *testing.T) {
|
||||||
re := captureOutput(t, func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||||||
debugPrintWARNINGDefault()
|
debugPrintWARNINGDefault()
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
})
|
})
|
||||||
m, e := getMinVer(runtime.Version())
|
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||||
if e == nil && m < ginSupportMinGoVer {
|
}
|
||||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.23+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
|
||||||
} else {
|
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
|
||||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
runtimeVersion = "go1.23.12"
|
||||||
}
|
re := captureOutput(t, func() {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
debugPrintWARNINGDefault()
|
||||||
|
SetMode(TestMode)
|
||||||
|
})
|
||||||
|
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||||
|
|||||||
16
doc.go
16
doc.go
@ -2,5 +2,21 @@
|
|||||||
Package gin implements a HTTP web framework called gin.
|
Package gin implements a HTTP web framework called gin.
|
||||||
|
|
||||||
See https://gin-gonic.com/ for more information about 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"
|
package gin // import "github.com/gin-gonic/gin"
|
||||||
|
|||||||
221
docs/doc.md
221
docs/doc.md
@ -22,6 +22,7 @@
|
|||||||
- [How to write log file](#how-to-write-log-file)
|
- [How to write log file](#how-to-write-log-file)
|
||||||
- [Custom Log Format](#custom-log-format)
|
- [Custom Log Format](#custom-log-format)
|
||||||
- [Controlling Log output coloring](#controlling-log-output-coloring)
|
- [Controlling Log output coloring](#controlling-log-output-coloring)
|
||||||
|
- [Avoid logging query strings](#avoid-loging-query-strings)
|
||||||
- [Model binding and validation](#model-binding-and-validation)
|
- [Model binding and validation](#model-binding-and-validation)
|
||||||
- [Custom Validators](#custom-validators)
|
- [Custom Validators](#custom-validators)
|
||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
@ -63,6 +64,7 @@
|
|||||||
- [http2 server push](#http2-server-push)
|
- [http2 server push](#http2-server-push)
|
||||||
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
|
||||||
- [Set and get a cookie](#set-and-get-a-cookie)
|
- [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)
|
- [Don't trust all proxies](#dont-trust-all-proxies)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
|
|
||||||
@ -84,10 +86,10 @@ go build -tags=jsoniter .
|
|||||||
go build -tags=go_json .
|
go build -tags=go_json .
|
||||||
```
|
```
|
||||||
|
|
||||||
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu supports avx instruction.)
|
[sonic](https://github.com/bytedance/sonic)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ go build -tags="sonic avx" .
|
$ go build -tags=sonic .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build without `MsgPack` rendering feature
|
### Build without `MsgPack` rendering feature
|
||||||
@ -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
|
### Model binding and validation
|
||||||
|
|
||||||
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).
|
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).
|
||||||
@ -872,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:
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -910,7 +926,7 @@ curl -X POST http://localhost:8080/person
|
|||||||
|
|
||||||
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
||||||
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
||||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
|
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
|
||||||
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
||||||
|
|
||||||
|
|
||||||
@ -1008,12 +1024,68 @@ curl -v localhost:8088/thinkerou/not-uuid
|
|||||||
|
|
||||||
### Bind custom unmarshaler
|
### Bind custom unmarshaler
|
||||||
|
|
||||||
|
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"encoding"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Birthday string
|
||||||
|
|
||||||
|
func (b *Birthday) UnmarshalText(text []byte) error {
|
||||||
|
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
var request struct {
|
||||||
|
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
|
||||||
|
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||||
|
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||||
|
}
|
||||||
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
|
_ = ctx.BindQuery(&request)
|
||||||
|
ctx.JSON(200, request)
|
||||||
|
})
|
||||||
|
_ = route.Run(":8088")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||||
|
```
|
||||||
|
Result
|
||||||
|
```sh
|
||||||
|
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
|
||||||
|
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If a type already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Birthday string
|
type Birthday string
|
||||||
@ -1023,29 +1095,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
route := gin.Default()
|
route := gin.Default()
|
||||||
var request struct {
|
var request struct {
|
||||||
Birthday Birthday `form:"birthday"`
|
Birthday Birthday `form:"birthday"`
|
||||||
|
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
|
||||||
|
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
|
||||||
}
|
}
|
||||||
route.GET("/test", func(ctx *gin.Context) {
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
_ = ctx.BindQuery(&request)
|
_ = ctx.BindQuery(&request)
|
||||||
ctx.JSON(200, request.Birthday)
|
ctx.JSON(200, request)
|
||||||
})
|
})
|
||||||
route.Run(":8088")
|
_ = route.Run(":8088")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl 'localhost:8088/test?birthday=2000-01-01'
|
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||||
```
|
```
|
||||||
Result
|
Result
|
||||||
```sh
|
```sh
|
||||||
"2000/01/01"
|
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag.
|
||||||
|
- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client.
|
||||||
|
|
||||||
### Bind Header
|
### Bind Header
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -1393,13 +1473,19 @@ func main() {
|
|||||||
|
|
||||||
### HTML rendering
|
### HTML rendering
|
||||||
|
|
||||||
Using LoadHTMLGlob() or LoadHTMLFiles()
|
Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
//go:embed templates/*
|
||||||
|
var templates embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.LoadHTMLGlob("templates/*")
|
router.LoadHTMLGlob("templates/*")
|
||||||
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
|
//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) {
|
router.GET("/index", func(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||||
"title": "Main website",
|
"title": "Main website",
|
||||||
@ -2298,12 +2384,64 @@ func main() {
|
|||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|
||||||
router.GET("/cookie", func(c *gin.Context) {
|
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()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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")
|
cookie, err := c.Cookie("gin_cookie")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cookie = "NotSet"
|
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)
|
fmt.Printf("Cookie value: %s \n", cookie)
|
||||||
@ -2313,6 +2451,65 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
## Don't trust all proxies
|
||||||
|
|
||||||
Gin lets you specify which headers to hold the real client IP (if any),
|
Gin lets you specify which headers to hold the real client IP (if any),
|
||||||
|
|||||||
10
errors.go
10
errors.go
@ -9,7 +9,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"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.
|
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
|
||||||
@ -26,8 +26,6 @@ const (
|
|||||||
ErrorTypePublic ErrorType = 1 << 1
|
ErrorTypePublic ErrorType = 1 << 1
|
||||||
// ErrorTypeAny indicates any other error.
|
// ErrorTypeAny indicates any other error.
|
||||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||||
// ErrorTypeNu indicates any other error.
|
|
||||||
ErrorTypeNu = 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents a error's specification.
|
// Error represents a error's specification.
|
||||||
@ -77,7 +75,7 @@ func (msg *Error) JSON() any {
|
|||||||
|
|
||||||
// MarshalJSON implements the json.Marshaller interface.
|
// MarshalJSON implements the json.Marshaller interface.
|
||||||
func (msg *Error) MarshalJSON() ([]byte, error) {
|
func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(msg.JSON())
|
return json.API.Marshal(msg.JSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface.
|
// 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()
|
// 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
|
return msg.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +155,7 @@ func (a errorMsgs) JSON() any {
|
|||||||
|
|
||||||
// MarshalJSON implements the json.Marshaller interface.
|
// MarshalJSON implements the json.Marshaller interface.
|
||||||
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(a.JSON())
|
return json.API.Marshal(a.JSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a errorMsgs) String() string {
|
func (a errorMsgs) String() string {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -33,8 +33,8 @@ func TestError(t *testing.T) {
|
|||||||
"meta": "some data",
|
"meta": "some data",
|
||||||
}, err.JSON())
|
}, err.JSON())
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(err)
|
jsonBytes, _ := json.API.Marshal(err)
|
||||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||||
|
|
||||||
err.SetMeta(H{ //nolint: errcheck
|
err.SetMeta(H{ //nolint: errcheck
|
||||||
"status": "200",
|
"status": "200",
|
||||||
@ -92,14 +92,14 @@ Error #03: third
|
|||||||
H{"error": "second", "meta": "some data"},
|
H{"error": "second", "meta": "some data"},
|
||||||
H{"error": "third", "status": "400"},
|
H{"error": "third", "status": "400"},
|
||||||
}, errs.JSON())
|
}, errs.JSON())
|
||||||
jsonBytes, _ := json.Marshal(errs)
|
jsonBytes, _ := json.API.Marshal(errs)
|
||||||
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||||
errs = errorMsgs{
|
errs = errorMsgs{
|
||||||
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||||
}
|
}
|
||||||
assert.Equal(t, H{"error": "first"}, errs.JSON())
|
assert.Equal(t, H{"error": "first"}, errs.JSON())
|
||||||
jsonBytes, _ = json.Marshal(errs)
|
jsonBytes, _ = json.API.Marshal(errs)
|
||||||
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
|
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||||
|
|
||||||
errs = errorMsgs{}
|
errs = errorMsgs{}
|
||||||
assert.Nil(t, errs.Last())
|
assert.Nil(t, errs.Last())
|
||||||
@ -126,4 +126,15 @@ func TestErrorUnwrap(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, innerErr)
|
require.ErrorIs(t, err, innerErr)
|
||||||
var testErr TestErr
|
var testErr TestErr
|
||||||
require.ErrorAs(t, err, &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
1
fs.go
@ -17,7 +17,6 @@ type OnlyFilesFS struct {
|
|||||||
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
|
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
|
||||||
func (o OnlyFilesFS) Open(name string) (http.File, error) {
|
func (o OnlyFilesFS) Open(name string) (http.File, error) {
|
||||||
f, err := o.FileSystem.Open(name)
|
f, err := o.FileSystem.Open(name)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
97
gin.go
97
gin.go
@ -11,22 +11,23 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
|
filesystem "github.com/gin-gonic/gin/internal/fs"
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
|
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
const (
|
||||||
const escapedColon = "\\:"
|
defaultMultipartMemory = 32 << 20 // 32 MB
|
||||||
const colon = ":"
|
escapedColon = "\\:"
|
||||||
const backslash = "\\"
|
colon = ":"
|
||||||
|
backslash = "\\"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
default404Body = []byte("404 page not found")
|
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.
|
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||||
type HandlerFunc func(*Context)
|
type HandlerFunc func(*Context)
|
||||||
|
|
||||||
@ -94,6 +92,10 @@ const (
|
|||||||
type Engine struct {
|
type Engine struct {
|
||||||
RouterGroup
|
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
|
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
|
||||||
// handler for the path with (without) the trailing slash exists.
|
// handler for the path with (without) the trailing slash exists.
|
||||||
// For example if /foo/ is requested but a route only exists for /foo, the
|
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||||
@ -133,10 +135,16 @@ type Engine struct {
|
|||||||
AppEngine bool
|
AppEngine bool
|
||||||
|
|
||||||
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
// 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
|
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.
|
// 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.
|
// as url.Path gonna be used, which is already unescaped.
|
||||||
UnescapePathValues bool
|
UnescapePathValues bool
|
||||||
|
|
||||||
@ -189,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
|
|||||||
// - HandleMethodNotAllowed: false
|
// - HandleMethodNotAllowed: false
|
||||||
// - ForwardedByClientIP: true
|
// - ForwardedByClientIP: true
|
||||||
// - UseRawPath: false
|
// - UseRawPath: false
|
||||||
|
// - UseEscapedPath: false
|
||||||
// - UnescapePathValues: true
|
// - UnescapePathValues: true
|
||||||
func New(opts ...OptionFunc) *Engine {
|
func New(opts ...OptionFunc) *Engine {
|
||||||
debugPrintWARNINGNew()
|
debugPrintWARNINGNew()
|
||||||
@ -206,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
|||||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||||
TrustedPlatform: defaultPlatform,
|
TrustedPlatform: defaultPlatform,
|
||||||
UseRawPath: false,
|
UseRawPath: false,
|
||||||
|
UseEscapedPath: false,
|
||||||
RemoveExtraSlash: false,
|
RemoveExtraSlash: false,
|
||||||
UnescapePathValues: true,
|
UnescapePathValues: true,
|
||||||
MaxMultipartMemory: defaultMultipartMemory,
|
MaxMultipartMemory: defaultMultipartMemory,
|
||||||
@ -215,7 +225,7 @@ func New(opts ...OptionFunc) *Engine {
|
|||||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||||
trustedCIDRs: defaultTrustedCIDRs,
|
trustedCIDRs: defaultTrustedCIDRs,
|
||||||
}
|
}
|
||||||
engine.RouterGroup.engine = engine
|
engine.engine = engine
|
||||||
engine.pool.New = func() any {
|
engine.pool.New = func() any {
|
||||||
return engine.allocateContext(engine.maxParams)
|
return engine.allocateContext(engine.maxParams)
|
||||||
}
|
}
|
||||||
@ -285,6 +295,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
|
|||||||
engine.SetHTMLTemplate(templ)
|
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.
|
// SetHTMLTemplate associate a template with HTML renderer.
|
||||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||||
if len(engine.trees) > 0 {
|
if len(engine.trees) > 0 {
|
||||||
@ -321,7 +344,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
|||||||
return engine
|
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 {
|
func (engine *Engine) With(opts ...OptionFunc) *Engine {
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(engine)
|
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:
|
// 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) {
|
func (engine *Engine) Routes() (routes RoutesInfo) {
|
||||||
for _, tree := range engine.trees {
|
for _, tree := range engine.trees {
|
||||||
routes = iterate("", tree.method, routes, tree.root)
|
routes = iterate("", tree.method, routes, tree.root)
|
||||||
@ -524,7 +547,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
|||||||
engine.updateRouteTrees()
|
engine.updateRouteTrees()
|
||||||
address := resolveAddress(addr)
|
address := resolveAddress(addr)
|
||||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
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
|
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.")
|
"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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
defer os.Remove(file)
|
defer os.Remove(file)
|
||||||
|
|
||||||
err = http.Serve(listener, engine.Handler())
|
server := &http.Server{ // #nosec G112
|
||||||
|
Handler: engine.Handler(),
|
||||||
|
}
|
||||||
|
err = server.Serve(listener)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||||
|
defer f.Close()
|
||||||
listener, err := net.FileListener(f)
|
listener, err := net.FileListener(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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.")
|
"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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP conforms to the http.Handler interface.
|
// ServeHTTP conforms to the http.Handler interface.
|
||||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
engine.routeTreesUpdated.Do(func() {
|
||||||
|
engine.updateRouteTrees()
|
||||||
|
})
|
||||||
|
|
||||||
c := engine.pool.Get().(*Context)
|
c := engine.pool.Get().(*Context)
|
||||||
c.writermem.reset(w)
|
c.writermem.reset(w)
|
||||||
c.Request = req
|
c.Request = req
|
||||||
@ -649,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
httpMethod := c.Request.Method
|
httpMethod := c.Request.Method
|
||||||
rPath := c.Request.URL.Path
|
rPath := c.Request.URL.Path
|
||||||
unescape := false
|
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
|
rPath = c.Request.URL.RawPath
|
||||||
unescape = engine.UnescapePathValues
|
unescape = engine.UnescapePathValues
|
||||||
}
|
}
|
||||||
@ -736,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
req := c.Request
|
req := c.Request
|
||||||
p := req.URL.Path
|
p := req.URL.Path
|
||||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
prefix = sanitizePathChars(prefix)
|
||||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
prefix = removeRepeatedChar(prefix, '/')
|
||||||
|
|
||||||
p = prefix + "/" + req.URL.Path
|
p = prefix + "/" + req.URL.Path
|
||||||
}
|
}
|
||||||
@ -748,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
redirectRequest(c)
|
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 {
|
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
rPath := req.URL.Path
|
rPath := req.URL.Path
|
||||||
|
|||||||
17
ginS/gins.go
17
ginS/gins.go
@ -12,15 +12,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
var once sync.Once
|
var engine = sync.OnceValue(func() *gin.Engine {
|
||||||
var internalEngine *gin.Engine
|
return gin.Default()
|
||||||
|
})
|
||||||
func engine() *gin.Engine {
|
|
||||||
once.Do(func() {
|
|
||||||
internalEngine = gin.Default()
|
|
||||||
})
|
|
||||||
return internalEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
|
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
|
||||||
func LoadHTMLGlob(pattern string) {
|
func LoadHTMLGlob(pattern string) {
|
||||||
@ -32,6 +26,11 @@ func LoadHTMLFiles(files ...string) {
|
|||||||
engine().LoadHTMLFiles(files...)
|
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.
|
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
|
||||||
func SetHTMLTemplate(templ *template.Template) {
|
func SetHTMLTemplate(templ *template.Template) {
|
||||||
engine().SetHTMLTemplate(templ)
|
engine().SetHTMLTemplate(templ)
|
||||||
|
|||||||
246
ginS/gins_test.go
Normal file
246
ginS/gins_test.go
Normal 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)
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -28,7 +29,6 @@ import (
|
|||||||
// params[1]=response status (custom compare status) default:"200 OK"
|
// params[1]=response status (custom compare status) default:"200 OK"
|
||||||
// params[2]=response body (custom compare content) default:"it worked"
|
// params[2]=response body (custom compare content) default:"it worked"
|
||||||
func testRequest(t *testing.T, params ...string) {
|
func testRequest(t *testing.T, params ...string) {
|
||||||
|
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
t.Fatal("url cannot be empty")
|
t.Fatal("url cannot be empty")
|
||||||
}
|
}
|
||||||
@ -47,12 +47,12 @@ func testRequest(t *testing.T, params ...string) {
|
|||||||
body, ioerr := io.ReadAll(resp.Body)
|
body, ioerr := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, ioerr)
|
require.NoError(t, ioerr)
|
||||||
|
|
||||||
var responseStatus = "200 OK"
|
responseStatus := "200 OK"
|
||||||
if len(params) > 1 && params[1] != "" {
|
if len(params) > 1 && params[1] != "" {
|
||||||
responseStatus = params[1]
|
responseStatus = params[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseBody = "it worked"
|
responseBody := "it worked"
|
||||||
if len(params) > 2 && params[2] != "" {
|
if len(params) > 2 && params[2] != "" {
|
||||||
responseBody = 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") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run())
|
assert.NoError(t, router.Run())
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:8080/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":8080"))
|
require.Error(t, router.Run(":8080"))
|
||||||
testRequest(t, "http://localhost:8080/example")
|
testRequest(t, "http://localhost:8080/example")
|
||||||
@ -170,7 +171,7 @@ func TestRunTLS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPusher(t *testing.T) {
|
func TestPusher(t *testing.T) {
|
||||||
var html = template.Must(template.New("https").Parse(`
|
html := template.Must(template.New("https").Parse(`
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Https Test</title>
|
<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") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run())
|
assert.NoError(t, router.Run())
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:3123/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":3123"))
|
require.Error(t, router.Run(":3123"))
|
||||||
testRequest(t, "http://localhost:3123/example")
|
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") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run(":5150"))
|
assert.NoError(t, router.Run(":5150"))
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:5150/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":5150"))
|
require.Error(t, router.Run(":5150"))
|
||||||
testRequest(t, "http://localhost:5150/example")
|
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")
|
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||||
scanner := bufio.NewScanner(c)
|
scanner := bufio.NewScanner(c)
|
||||||
var response string
|
var responseBuilder strings.Builder
|
||||||
for scanner.Scan() {
|
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, "HTTP/1.0 200", "should get a 200")
|
||||||
assert.Contains(t, response, "it worked", "resp body should match")
|
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")
|
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||||
scanner := bufio.NewScanner(c)
|
scanner := bufio.NewScanner(c)
|
||||||
var response string
|
var responseBuilder strings.Builder
|
||||||
for scanner.Scan() {
|
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, "HTTP/1.0 200", "should get a 200")
|
||||||
assert.Contains(t, response, "it worked", "resp body should match")
|
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")
|
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||||
scanner := bufio.NewScanner(c)
|
scanner := bufio.NewScanner(c)
|
||||||
var response string
|
var responseBuilder strings.Builder
|
||||||
for scanner.Scan() {
|
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, "HTTP/1.0 200", "should get a 200")
|
||||||
assert.Contains(t, response, "it worked", "resp body should match")
|
assert.Contains(t, response, "it worked", "resp body should match")
|
||||||
}
|
}
|
||||||
@ -394,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
iterations := 200
|
iterations := 200
|
||||||
wg.Add(iterations)
|
wg.Add(iterations)
|
||||||
for i := 0; i < iterations; i++ {
|
for range iterations {
|
||||||
go func() {
|
go func() {
|
||||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
288
gin_test.go
288
gin_test.go
@ -46,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
|
|||||||
})
|
})
|
||||||
router.GET("/raw", func(c *Context) {
|
router.GET("/raw", func(c *Context) {
|
||||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
|
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) {
|
func TestH2c(t *testing.T) {
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
ln, err := net.Listen("tcp", localhostIP+":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@ -325,6 +325,115 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
|||||||
assert.Equal(t, "Date: 2017/07/01", string(resp))
|
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) {
|
func TestAddRoute(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
|
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
|
||||||
@ -436,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRebuild404Handlers(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) {
|
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||||
@ -611,6 +743,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
|
|||||||
assert.Equal(t, int64(1), handlerCounterV2)
|
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) {
|
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||||
r := New()
|
r := New()
|
||||||
|
|
||||||
@ -738,7 +919,7 @@ func handlerTest1(c *Context) {}
|
|||||||
func handlerTest2(c *Context) {}
|
func handlerTest2(c *Context) {}
|
||||||
|
|
||||||
func TestNewOptionFunc(t *testing.T) {
|
func TestNewOptionFunc(t *testing.T) {
|
||||||
var fc = func(e *Engine) {
|
fc := func(e *Engine) {
|
||||||
e.GET("/test1", handlerTest1)
|
e.GET("/test1", handlerTest1)
|
||||||
e.GET("/test2", handlerTest2)
|
e.GET("/test2", handlerTest2)
|
||||||
|
|
||||||
@ -774,7 +955,7 @@ func TestWithOptionFunc(t *testing.T) {
|
|||||||
type Birthday string
|
type Birthday string
|
||||||
|
|
||||||
func (b *Birthday) UnmarshalParam(param string) error {
|
func (b *Birthday) UnmarshalParam(param string) error {
|
||||||
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -804,3 +985,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
|
|||||||
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
|
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
|
||||||
assert.Equal(t, http.StatusNotFound, resp.Code)
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -298,8 +298,8 @@ func TestShouldBindUri(t *testing.T) {
|
|||||||
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||||
var person Person
|
var person Person
|
||||||
require.NoError(t, c.ShouldBindUri(&person))
|
require.NoError(t, c.ShouldBindUri(&person))
|
||||||
assert.NotEqual(t, "", person.Name)
|
assert.NotEmpty(t, person.Name)
|
||||||
assert.NotEqual(t, "", person.ID)
|
assert.NotEmpty(t, person.ID)
|
||||||
c.String(http.StatusOK, "ShouldBindUri test OK")
|
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) {
|
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||||
var person Person
|
var person Person
|
||||||
require.NoError(t, c.BindUri(&person))
|
require.NoError(t, c.BindUri(&person))
|
||||||
assert.NotEqual(t, "", person.Name)
|
assert.NotEmpty(t, person.Name)
|
||||||
assert.NotEqual(t, "", person.ID)
|
assert.NotEmpty(t, person.ID)
|
||||||
c.String(http.StatusOK, "BindUri test OK")
|
c.String(http.StatusOK, "BindUri test OK")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
61
go.mod
61
go.mod
@ -1,46 +1,47 @@
|
|||||||
module github.com/gin-gonic/gin
|
module github.com/gin-gonic/gin
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.13.1
|
github.com/bytedance/sonic v1.15.0
|
||||||
github.com/gin-contrib/sse v0.1.0
|
github.com/gin-contrib/sse v1.1.0
|
||||||
github.com/go-playground/validator/v10 v10.22.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/goccy/go-json v0.10.2
|
github.com/goccy/go-json v0.10.5
|
||||||
|
github.com/goccy/go-yaml v1.19.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2
|
github.com/modern-go/reflect2 v1.0.2
|
||||||
github.com/quic-go/quic-go v0.48.2
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/quic-go/quic-go v0.59.0
|
||||||
github.com/ugorji/go/codec v1.2.12
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/net v0.37.0
|
github.com/ugorji/go/codec v1.3.1
|
||||||
google.golang.org/protobuf v1.34.1
|
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
golang.org/x/net v0.50.0
|
||||||
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // 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/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/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
|
||||||
golang.org/x/text v0.23.0 // indirect
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
138
go.sum
138
go.sum
@ -1,114 +1,96 @@
|
|||||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
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.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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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.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.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.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.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.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.9.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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
|
||||||
|
|||||||
@ -6,14 +6,17 @@ package bytesconv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
cRand "crypto/rand"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
var (
|
||||||
var testBytes = []byte(testString)
|
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||||
|
testBytes = []byte(testString)
|
||||||
|
)
|
||||||
|
|
||||||
func rawBytesToStr(b []byte) string {
|
func rawBytesToStr(b []byte) string {
|
||||||
return string(b)
|
return string(b)
|
||||||
@ -27,14 +30,26 @@ func rawStrToBytes(s string) []byte {
|
|||||||
|
|
||||||
func TestBytesToString(t *testing.T) {
|
func TestBytesToString(t *testing.T) {
|
||||||
data := make([]byte, 1024)
|
data := make([]byte, 1024)
|
||||||
for i := 0; i < 100; i++ {
|
for range 100 {
|
||||||
rand.Read(data)
|
_, err := cRand.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if rawBytesToStr(data) != BytesToString(data) {
|
if rawBytesToStr(data) != BytesToString(data) {
|
||||||
t.Fatal("don't match")
|
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 letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
const (
|
const (
|
||||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
@ -42,7 +57,7 @@ const (
|
|||||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
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 {
|
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
@ -64,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStringToBytes(t *testing.T) {
|
func TestStringToBytes(t *testing.T) {
|
||||||
for i := 0; i < 100; i++ {
|
for range 100 {
|
||||||
s := RandStringBytesMaskImprSrcSB(64)
|
s := RandStringBytesMaskImprSrcSB(64)
|
||||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||||
t.Fatal("don't match")
|
t.Fatal("don't match")
|
||||||
@ -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
|
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||||
|
|
||||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
rawBytesToStr(testBytes)
|
rawBytesToStr(testBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
BytesToString(testBytes)
|
BytesToString(testBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
rawStrToBytes(testString)
|
rawStrToBytes(testString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
StringToBytes(testString)
|
StringToBytes(testString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
internal/fs/fs.go
Normal file
21
internal/fs/fs.go
Normal 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
49
internal/fs/fs_test.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
49
logger.go
49
logger.go
@ -44,10 +44,15 @@ type LoggerConfig struct {
|
|||||||
// Optional. Default value is gin.DefaultWriter.
|
// Optional. Default value is gin.DefaultWriter.
|
||||||
Output io.Writer
|
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.
|
// Optional.
|
||||||
SkipPaths []string
|
SkipPaths []string
|
||||||
|
|
||||||
|
// SkipQueryString indicates that query strings should not be written
|
||||||
|
// for cases such as when API keys are passed via query strings.
|
||||||
|
// Optional. Default value is false.
|
||||||
|
SkipQueryString bool
|
||||||
|
|
||||||
// Skip is a Skipper that indicates which logs should not be written.
|
// Skip is a Skipper that indicates which logs should not be written.
|
||||||
// Optional.
|
// Optional.
|
||||||
Skip Skipper
|
Skip Skipper
|
||||||
@ -82,7 +87,7 @@ type LogFormatterParams struct {
|
|||||||
// BodySize is the size of the Response Body
|
// BodySize is the size of the Response Body
|
||||||
BodySize int
|
BodySize int
|
||||||
// Keys are the keys set on the request's context.
|
// 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.
|
// 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.
|
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
|
||||||
func (p *LogFormatterParams) MethodColor() string {
|
func (p *LogFormatterParams) MethodColor() string {
|
||||||
method := p.Method
|
method := p.Method
|
||||||
@ -139,20 +165,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
|
|||||||
|
|
||||||
// defaultLogFormatter is the default log format function Logger middleware uses.
|
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||||
var defaultLogFormatter = func(param LogFormatterParams) string {
|
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||||
var statusColor, methodColor, resetColor string
|
var statusColor, methodColor, resetColor, latencyColor string
|
||||||
if param.IsOutputColor() {
|
if param.IsOutputColor() {
|
||||||
statusColor = param.StatusCodeColor()
|
statusColor = param.StatusCodeColor()
|
||||||
methodColor = param.MethodColor()
|
methodColor = param.MethodColor()
|
||||||
resetColor = param.ResetColor()
|
resetColor = param.ResetColor()
|
||||||
|
latencyColor = param.LatencyColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
if param.Latency > time.Minute {
|
switch {
|
||||||
param.Latency = param.Latency.Truncate(time.Second)
|
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"),
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
statusColor, param.StatusCode, resetColor,
|
statusColor, param.StatusCode, resetColor,
|
||||||
param.Latency,
|
latencyColor, param.Latency, resetColor,
|
||||||
param.ClientIP,
|
param.ClientIP,
|
||||||
methodColor, param.Method, resetColor,
|
methodColor, param.Method, resetColor,
|
||||||
param.Path,
|
param.Path,
|
||||||
@ -270,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
|
|
||||||
param.BodySize = c.Writer.Size()
|
param.BodySize = c.Writer.Size()
|
||||||
|
|
||||||
if raw != "" {
|
if raw != "" && !conf.SkipQueryString {
|
||||||
path = path + "?" + raw
|
path = path + "?" + raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func TestLogger(t *testing.T) {
|
|||||||
|
|
||||||
// I wrote these first (extending the above) but then realized they are more
|
// I wrote these first (extending the above) but then realized they are more
|
||||||
// like integration tests because they test the whole logging process rather
|
// 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()
|
buffer.Reset()
|
||||||
PerformRequest(router, http.MethodPost, "/example")
|
PerformRequest(router, http.MethodPost, "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
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
|
// I wrote these first (extending the above) but then realized they are more
|
||||||
// like integration tests because they test the whole logging process rather
|
// 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()
|
buffer.Reset()
|
||||||
PerformRequest(router, http.MethodPost, "/example")
|
PerformRequest(router, http.MethodPost, "/example")
|
||||||
assert.Contains(t, buffer.String(), "200")
|
assert.Contains(t, buffer.String(), "200")
|
||||||
@ -181,7 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
|
|||||||
|
|
||||||
func TestLoggerWithConfigFormatting(t *testing.T) {
|
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||||
var gotParam LogFormatterParams
|
var gotParam LogFormatterParams
|
||||||
var gotKeys map[string]any
|
var gotKeys map[any]any
|
||||||
buffer := new(strings.Builder)
|
buffer := new(strings.Builder)
|
||||||
|
|
||||||
router := New()
|
router := New()
|
||||||
@ -277,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
|
|||||||
isTerm: false,
|
isTerm: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 | 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|\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| 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 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForMethod(t *testing.T) {
|
func TestColorForMethod(t *testing.T) {
|
||||||
@ -317,6 +317,23 @@ func TestColorForStatus(t *testing.T) {
|
|||||||
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestColorForLatency(t *testing.T) {
|
||||||
|
colorForLantency := func(latency time.Duration) string {
|
||||||
|
p := LogFormatterParams{
|
||||||
|
Latency: latency,
|
||||||
|
}
|
||||||
|
return p.LatencyColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
|
||||||
|
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
|
||||||
|
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
|
||||||
|
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
|
||||||
|
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
|
||||||
|
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
|
||||||
|
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
|
||||||
|
}
|
||||||
|
|
||||||
func TestResetColor(t *testing.T) {
|
func TestResetColor(t *testing.T) {
|
||||||
p := LogFormatterParams{}
|
p := LogFormatterParams{}
|
||||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
||||||
@ -371,11 +388,11 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
|
|
||||||
w := PerformRequest(router, http.MethodGet, "/error")
|
w := PerformRequest(router, http.MethodGet, "/error")
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
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")
|
w = PerformRequest(router, http.MethodGet, "/abort")
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
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")
|
w = PerformRequest(router, http.MethodGet, "/print")
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
@ -454,3 +471,17 @@ func TestForceConsoleColor(t *testing.T) {
|
|||||||
// reset console color mode.
|
// reset console color mode.
|
||||||
consoleColorMode = autoColor
|
consoleColorMode = autoColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigSkipQueryString(t *testing.T) {
|
||||||
|
buffer := new(strings.Builder)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
SkipQueryString: true,
|
||||||
|
}))
|
||||||
|
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
|
||||||
|
|
||||||
|
PerformRequest(router, "GET", "/logged?a=21")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.NotContains(t, buffer.String(), "a=21")
|
||||||
|
}
|
||||||
|
|||||||
@ -203,7 +203,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
|||||||
assert.Equal(t, "ACB", signature)
|
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
|
// as well as Abort
|
||||||
func TestMiddlewareFailHandlersChain(t *testing.T) {
|
func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||||
// SETUP
|
// SETUP
|
||||||
@ -249,5 +249,5 @@ func TestMiddlewareWrite(t *testing.T) {
|
|||||||
w := PerformRequest(router, http.MethodGet, "/")
|
w := PerformRequest(router, http.MethodGet, "/")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
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(), " ", ""))
|
||||||
}
|
}
|
||||||
|
|||||||
8
mode.go
8
mode.go
@ -44,8 +44,10 @@ var DefaultWriter io.Writer = os.Stdout
|
|||||||
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
|
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
|
||||||
var DefaultErrorWriter io.Writer = os.Stderr
|
var DefaultErrorWriter io.Writer = os.Stderr
|
||||||
|
|
||||||
var ginMode int32 = debugCode
|
var (
|
||||||
var modeName atomic.Value
|
ginMode int32 = debugCode
|
||||||
|
modeName atomic.Value
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
mode := os.Getenv(EnvGinMode)
|
mode := os.Getenv(EnvGinMode)
|
||||||
@ -63,7 +65,7 @@ func SetMode(value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch value {
|
switch value {
|
||||||
case DebugMode, "":
|
case DebugMode:
|
||||||
atomic.StoreInt32(&ginMode, debugCode)
|
atomic.StoreInt32(&ginMode, debugCode)
|
||||||
case ReleaseMode:
|
case ReleaseMode:
|
||||||
atomic.StoreInt32(&ginMode, releaseCode)
|
atomic.StoreInt32(&ginMode, releaseCode)
|
||||||
|
|||||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||||
// for p, eliminating . and .. elements.
|
// for p, eliminating . and .. elements.
|
||||||
//
|
//
|
||||||
@ -19,7 +21,6 @@ package gin
|
|||||||
//
|
//
|
||||||
// If the result of this process is an empty string, "/" is returned.
|
// If the result of this process is an empty string, "/" is returned.
|
||||||
func cleanPath(p string) string {
|
func cleanPath(p string) string {
|
||||||
const stackBufSize = 128
|
|
||||||
// Turn empty string into "/"
|
// Turn empty string into "/"
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return "/"
|
return "/"
|
||||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
|||||||
}
|
}
|
||||||
b[w] = c
|
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])
|
||||||
|
}
|
||||||
|
|||||||
53
path_test.go
53
path_test.go
@ -94,7 +94,7 @@ func TestPathCleanMallocs(t *testing.T) {
|
|||||||
func BenchmarkPathClean(b *testing.B) {
|
func BenchmarkPathClean(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
for _, test := range cleanTests {
|
for _, test := range cleanTests {
|
||||||
cleanPath(test.path)
|
cleanPath(test.path)
|
||||||
}
|
}
|
||||||
@ -134,12 +134,59 @@ func TestPathCleanLong(t *testing.T) {
|
|||||||
|
|
||||||
func BenchmarkPathCleanLong(b *testing.B) {
|
func BenchmarkPathCleanLong(b *testing.B) {
|
||||||
cleanTests := genLongPaths()
|
cleanTests := genLongPaths()
|
||||||
b.ResetTimer()
|
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for b.Loop() {
|
||||||
for _, test := range cleanTests {
|
for _, test := range cleanTests {
|
||||||
cleanPath(test.path)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
125
recovery.go
125
recovery.go
@ -5,25 +5,27 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
dunno = []byte("???")
|
dunno = "???"
|
||||||
centerDot = []byte("·")
|
stackSkip = 3
|
||||||
dot = []byte(".")
|
|
||||||
slash = []byte("/")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||||
@ -55,47 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
|||||||
}
|
}
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if rec := recover(); rec != nil {
|
||||||
// Check for a broken connection, as it is not really a
|
// Check for a broken connection, as it is not really a
|
||||||
// condition that warrants a panic stack trace.
|
// condition that warrants a panic stack trace.
|
||||||
var brokenPipe bool
|
var isBrokenPipe bool
|
||||||
if ne, ok := err.(*net.OpError); ok {
|
err, ok := rec.(error)
|
||||||
var se *os.SyscallError
|
if ok {
|
||||||
if errors.As(ne, &se) {
|
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
|
||||||
seStr := strings.ToLower(se.Error())
|
errors.Is(err, syscall.ECONNRESET) ||
|
||||||
if strings.Contains(seStr, "broken pipe") ||
|
errors.Is(err, http.ErrAbortHandler)
|
||||||
strings.Contains(seStr, "connection reset by peer") {
|
|
||||||
brokenPipe = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
stack := stack(3)
|
if isBrokenPipe {
|
||||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
|
||||||
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)
|
|
||||||
} else if IsDebugging() {
|
} else if IsDebugging() {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||||
timeFormat(time.Now()), headersToStr, err, stack, reset)
|
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
|
||||||
} else {
|
} else {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||||
timeFormat(time.Now()), err, stack, reset)
|
timeFormat(time.Now()), rec, stack(stackSkip), reset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if brokenPipe {
|
if isBrokenPipe {
|
||||||
// If the connection is dead, we can't write a status to it.
|
// If the connection is dead, we can't write a status to it.
|
||||||
c.Error(err.(error)) //nolint: errcheck
|
c.Error(err) //nolint: errcheck
|
||||||
c.Abort()
|
c.Abort()
|
||||||
} else {
|
} else {
|
||||||
handle(c, err)
|
handle(c, rec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -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) {
|
func defaultHandleRecovery(c *Context, _ any) {
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -112,8 +115,11 @@ func stack(skip int) []byte {
|
|||||||
buf := new(bytes.Buffer) // the returned data
|
buf := new(bytes.Buffer) // the returned data
|
||||||
// As we loop, we open files and read them. These variables record the currently
|
// As we loop, we open files and read them. These variables record the currently
|
||||||
// loaded file.
|
// loaded file.
|
||||||
var lines [][]byte
|
var (
|
||||||
var lastFile string
|
nLine string
|
||||||
|
lastFile string
|
||||||
|
err error
|
||||||
|
)
|
||||||
for i := skip; ; i++ { // Skip the expected number of frames
|
for i := skip; ; i++ { // Skip the expected number of frames
|
||||||
pc, file, line, ok := runtime.Caller(i)
|
pc, file, line, ok := runtime.Caller(i)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -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.
|
// Print this much at least. If we can't find the source, it won't show.
|
||||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := os.ReadFile(file)
|
nLine, err = readNthLine(file, line-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
|
||||||
lastFile = file
|
lastFile = file
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
|
||||||
}
|
}
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// source returns a space-trimmed slice of the n'th line.
|
// readNthLine reads the nth line from the file.
|
||||||
func source(lines [][]byte, n int) []byte {
|
// It returns the trimmed content of the line if found,
|
||||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
// or an empty string if the line doesn't exist.
|
||||||
if n < 0 || n >= len(lines) {
|
// If there's an error opening the file, it returns the error.
|
||||||
return dunno
|
func readNthLine(file string, n int) (string, error) {
|
||||||
|
if n < 0 {
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
return bytes.TrimSpace(lines[n])
|
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner.Scan() {
|
||||||
|
return strings.TrimSpace(scanner.Text()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// function returns, if possible, the name of the function containing the PC.
|
// function returns, if possible, the name of the function containing the PC.
|
||||||
func function(pc uintptr) []byte {
|
func function(pc uintptr) string {
|
||||||
fn := runtime.FuncForPC(pc)
|
fn := runtime.FuncForPC(pc)
|
||||||
if fn == nil {
|
if fn == nil {
|
||||||
return dunno
|
return dunno
|
||||||
}
|
}
|
||||||
name := []byte(fn.Name())
|
name := fn.Name()
|
||||||
// The name includes the path name to the package, which is unnecessary
|
// The name includes the path name to the package, which is unnecessary
|
||||||
// since the file name is already included. Plus, it has center dots.
|
// since the file name is already included. Plus, it has center dots.
|
||||||
// That is, we see
|
// That is, we see
|
||||||
@ -158,13 +183,13 @@ func function(pc uintptr) []byte {
|
|||||||
// *T.ptrmethod
|
// *T.ptrmethod
|
||||||
// Also the package path might contain dot (e.g. code.google.com/...),
|
// Also the package path might contain dot (e.g. code.google.com/...),
|
||||||
// so first eliminate the path prefix
|
// 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:]
|
name = name[lastSlash+1:]
|
||||||
}
|
}
|
||||||
if period := bytes.Index(name, dot); period >= 0 {
|
if period := strings.IndexByte(name, '.'); period >= 0 {
|
||||||
name = name[period+1:]
|
name = name[period+1:]
|
||||||
}
|
}
|
||||||
name = bytes.ReplaceAll(name, centerDot, dot)
|
name = strings.ReplaceAll(name, "·", ".")
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
190
recovery_test.go
190
recovery_test.go
@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
|
|||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
router.GET("/recovery", func(c *Context) {
|
router.GET("/recovery", func(c *Context) {
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery",
|
w := PerformRequest(router, http.MethodGet, "/recovery",
|
||||||
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(RecoveryWithWriter(buffer))
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||||
assert.Contains(t, buffer.String(), t.Name())
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
router.Use(RecoveryWithWriter(nil))
|
router.Use(RecoveryWithWriter(nil))
|
||||||
router.GET("/recovery", func(c *Context) {
|
router.GET("/recovery", func(c *Context) {
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSource(t *testing.T) {
|
|
||||||
bs := source(nil, 0)
|
|
||||||
assert.Equal(t, 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) {
|
func TestFunction(t *testing.T) {
|
||||||
bs := function(1)
|
bs := function(1)
|
||||||
assert.Equal(t, dunno, bs)
|
assert.Equal(t, dunno, bs)
|
||||||
@ -113,13 +98,13 @@ func TestFunction(t *testing.T) {
|
|||||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||||
const expectCode = 204
|
const expectCode = 204
|
||||||
|
|
||||||
expectMsgs := map[syscall.Errno]string{
|
expectErrnos := []syscall.Errno{
|
||||||
syscall.EPIPE: "broken pipe",
|
syscall.EPIPE,
|
||||||
syscall.ECONNRESET: "connection reset by peer",
|
syscall.ECONNRESET,
|
||||||
}
|
}
|
||||||
|
|
||||||
for errno, expectMsg := range expectMsgs {
|
for _, errno := range expectErrnos {
|
||||||
t.Run(expectMsg, func(t *testing.T) {
|
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
|
|
||||||
router := New()
|
router := New()
|
||||||
@ -137,11 +122,36 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
|||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, expectCode, w.Code)
|
assert.Equal(t, expectCode, w.Code)
|
||||||
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
|
||||||
|
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||||
errBuffer := new(strings.Builder)
|
errBuffer := new(strings.Builder)
|
||||||
buffer := new(strings.Builder)
|
buffer := new(strings.Builder)
|
||||||
@ -152,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||||
assert.Contains(t, buffer.String(), t.Name())
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -171,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
@ -187,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(CustomRecovery(handleRecovery))
|
router.Use(CustomRecovery(handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||||
assert.Contains(t, buffer.String(), t.Name())
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -206,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
@ -222,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||||
router.GET("/recovery", func(_ *Context) {
|
router.GET("/recovery", func(_ *Context) {
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oops, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||||
// TEST
|
// TEST
|
||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "panic recovered")
|
assert.Contains(t, buffer.String(), "panic recovered")
|
||||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||||
assert.Contains(t, buffer.String(), t.Name())
|
assert.Contains(t, buffer.String(), t.Name())
|
||||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
@ -241,7 +251,119 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||||
|
|
||||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||||
|
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
34
render/bson.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BSON contains the given interface object.
|
||||||
|
type BSON struct {
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
var bsonContentType = []string{"application/bson"}
|
||||||
|
|
||||||
|
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
|
||||||
|
func (r BSON) Render(w http.ResponseWriter) error {
|
||||||
|
r.WriteContentType(w)
|
||||||
|
|
||||||
|
bytes, err := bson.Marshal(&r.Data)
|
||||||
|
if err == nil {
|
||||||
|
_, err = w.Write(bytes)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
|
||||||
|
func (r BSON) WriteContentType(w http.ResponseWriter) {
|
||||||
|
writeContentType(w, bsonContentType)
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@ package render
|
|||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
||||||
@ -31,10 +33,12 @@ type HTMLProduction struct {
|
|||||||
|
|
||||||
// HTMLDebug contains template delims and pattern and function with file list.
|
// HTMLDebug contains template delims and pattern and function with file list.
|
||||||
type HTMLDebug struct {
|
type HTMLDebug struct {
|
||||||
Files []string
|
Files []string
|
||||||
Glob string
|
Glob string
|
||||||
Delims Delims
|
FileSystem http.FileSystem
|
||||||
FuncMap template.FuncMap
|
Patterns []string
|
||||||
|
Delims Delims
|
||||||
|
FuncMap template.FuncMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML contains template reference and its name with given interface object.
|
// HTML contains template reference and its name with given interface object.
|
||||||
@ -63,6 +67,7 @@ func (r HTMLDebug) Instance(name string, data any) Render {
|
|||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r HTMLDebug) loadTemplate() *template.Template {
|
func (r HTMLDebug) loadTemplate() *template.Template {
|
||||||
if r.FuncMap == nil {
|
if r.FuncMap == nil {
|
||||||
r.FuncMap = template.FuncMap{}
|
r.FuncMap = template.FuncMap{}
|
||||||
@ -73,7 +78,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
|
|||||||
if r.Glob != "" {
|
if r.Glob != "" {
|
||||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(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.
|
// Render (HTML) executes template and writes its result with custom ContentType for response.
|
||||||
|
|||||||
@ -9,9 +9,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON contains the given interface object.
|
// 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.
|
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||||
func WriteJSON(w http.ResponseWriter, obj any) error {
|
func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||||
writeContentType(w, jsonContentType)
|
writeContentType(w, jsonContentType)
|
||||||
jsonBytes, err := json.Marshal(obj)
|
jsonBytes, err := json.API.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
|
||||||
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
jsonBytes, err := json.Marshal(r.Data)
|
jsonBytes, err := json.API.Marshal(r.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// 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) {
|
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
ret, err := json.Marshal(r.Data)
|
ret, err := json.API.Marshal(r.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// 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)
|
r.WriteContentType(w)
|
||||||
ret, err := json.Marshal(r.Data)
|
ret, err := json.API.Marshal(r.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
||||||
|
|
||||||
for _, r := range bytesconv.BytesToString(ret) {
|
for _, r := range bytesconv.BytesToString(ret) {
|
||||||
cvt := string(r)
|
if r > unicode.MaxASCII {
|
||||||
if r >= 128 {
|
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
||||||
cvt = fmt.Sprintf("\\u%04x", int64(r))
|
buffer.Write(escapeBuf)
|
||||||
|
} else {
|
||||||
|
buffer.WriteByte(byte(r))
|
||||||
}
|
}
|
||||||
buffer.WriteString(cvt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = w.Write(buffer.Bytes())
|
_, 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.
|
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||||
func (r PureJSON) Render(w http.ResponseWriter) error {
|
func (r PureJSON) Render(w http.ResponseWriter) error {
|
||||||
r.WriteContentType(w)
|
r.WriteContentType(w)
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.API.NewEncoder(w)
|
||||||
encoder.SetEscapeHTML(false)
|
encoder.SetEscapeHTML(false)
|
||||||
return encoder.Encode(r.Data)
|
return encoder.Encode(r.Data)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
|
|||||||
}
|
}
|
||||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||||
}
|
}
|
||||||
r.writeHeaders(w, r.Headers)
|
r.writeHeaders(w)
|
||||||
_, err = io.Copy(w, r.Reader)
|
_, err = io.Copy(w, r.Reader)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
|
|||||||
writeContentType(w, []string{r.ContentType})
|
writeContentType(w, []string{r.ContentType})
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHeaders writes custom Header.
|
// writeHeaders writes headers from r.Headers into response.
|
||||||
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
|
func (r Reader) writeHeaders(w http.ResponseWriter) {
|
||||||
header := w.Header()
|
header := w.Header()
|
||||||
for k, v := range headers {
|
for k, v := range r.Headers {
|
||||||
if header.Get(k) == "" {
|
if header.Get(k) == "" {
|
||||||
header.Set(k, v)
|
header.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -16,9 +16,6 @@ import (
|
|||||||
"github.com/ugorji/go/codec"
|
"github.com/ugorji/go/codec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO unit tests
|
|
||||||
// test errors
|
|
||||||
|
|
||||||
func TestRenderMsgPack(t *testing.T) {
|
func TestRenderMsgPack(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
h := new(codec.MsgpackHandle)
|
var decoded map[string]any
|
||||||
assert.NotNil(t, h)
|
var mh codec.MsgpackHandle
|
||||||
buf := bytes.NewBuffer([]byte{})
|
mh.RawToString = true
|
||||||
assert.NotNil(t, buf)
|
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
|
||||||
err = codec.NewEncoder(buf, h).Encode(data)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, w.Body.String(), buf.String())
|
assert.Equal(t, data, decoded)
|
||||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteMsgPack(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"num": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := WriteMsgPack(w, data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
var decoded map[string]any
|
||||||
|
var mh codec.MsgpackHandle
|
||||||
|
mh.RawToString = true
|
||||||
|
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, decoded, 2)
|
||||||
|
assert.Equal(t, "bar", decoded["foo"])
|
||||||
|
assert.EqualValues(t, 42, decoded["num"])
|
||||||
|
}
|
||||||
|
|
||||||
|
type failWriter struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *failWriter) Write(data []byte) (int, error) {
|
||||||
|
return 0, errors.New("write error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderMsgPackError(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (MsgPack{data}).Render(&failWriter{w})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "write error")
|
||||||
|
}
|
||||||
|
|||||||
@ -15,10 +15,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/internal/json"
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ func TestRenderJSON(t *testing.T) {
|
|||||||
err := (JSON{data}).Render(w)
|
err := (JSON{data}).Render(w)
|
||||||
|
|
||||||
require.NoError(t, err)
|
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"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ func TestRenderIndentedJSON(t *testing.T) {
|
|||||||
err := (IndentedJSON{data}).Render(w)
|
err := (IndentedJSON{data}).Render(w)
|
||||||
|
|
||||||
require.NoError(t, err)
|
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"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ func TestRenderSecureJSON(t *testing.T) {
|
|||||||
err1 := (SecureJSON{"while(1);", data}).Render(w1)
|
err1 := (SecureJSON{"while(1);", data}).Render(w1)
|
||||||
|
|
||||||
require.NoError(t, err1)
|
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"))
|
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
@ -173,7 +174,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
|
|||||||
err = jsonpJSON.Render(ew)
|
err = jsonpJSON.Render(ew)
|
||||||
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||||
|
|
||||||
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||||
ew.bufString = string(data)
|
ew.bufString = string(data)
|
||||||
err = jsonpJSON.Render(ew)
|
err = jsonpJSON.Render(ew)
|
||||||
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||||
@ -194,7 +195,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
|
|||||||
e := (JsonpJSON{"", data}).Render(w)
|
e := (JsonpJSON{"", data}).Render(w)
|
||||||
require.NoError(t, e)
|
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"))
|
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +218,7 @@ func TestRenderAsciiJSON(t *testing.T) {
|
|||||||
err := (AsciiJSON{data1}).Render(w1)
|
err := (AsciiJSON{data1}).Render(w1)
|
||||||
|
|
||||||
require.NoError(t, err)
|
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"))
|
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
|
||||||
|
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
@ -244,7 +245,7 @@ func TestRenderPureJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := (PureJSON{data}).Render(w)
|
err := (PureJSON{data}).Render(w)
|
||||||
require.NoError(t, err)
|
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"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +286,14 @@ b:
|
|||||||
|
|
||||||
err := (YAML{data}).Render(w)
|
err := (YAML{data}).Render(w)
|
||||||
require.NoError(t, err)
|
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"))
|
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +360,31 @@ func TestRenderProtoBufFail(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderBSON(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
reps := []int64{int64(1), int64(2)}
|
||||||
|
type mystruct struct {
|
||||||
|
Label string
|
||||||
|
Reps []int64
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &mystruct{
|
||||||
|
Label: "test",
|
||||||
|
Reps: reps,
|
||||||
|
}
|
||||||
|
|
||||||
|
(BSON{data}).WriteContentType(w)
|
||||||
|
bsonData, err := bson.Marshal(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
err = (BSON{data}).Render(w)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, bsonData, w.Body.Bytes())
|
||||||
|
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderXML(t *testing.T) {
|
func TestRenderXML(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := xmlmap{
|
data := xmlmap{
|
||||||
@ -489,10 +522,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
|||||||
func TestRenderHTMLDebugFiles(t *testing.T) {
|
func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
htmlRender := HTMLDebug{
|
htmlRender := HTMLDebug{
|
||||||
Files: []string{"../testdata/template/hello.tmpl"},
|
Files: []string{"../testdata/template/hello.tmpl"},
|
||||||
Glob: "",
|
Glob: "",
|
||||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
FileSystem: nil,
|
||||||
FuncMap: nil,
|
Patterns: nil,
|
||||||
|
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||||
|
FuncMap: nil,
|
||||||
}
|
}
|
||||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||||
"name": "thinkerou",
|
"name": "thinkerou",
|
||||||
@ -508,10 +543,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
|
|||||||
func TestRenderHTMLDebugGlob(t *testing.T) {
|
func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
htmlRender := HTMLDebug{
|
htmlRender := HTMLDebug{
|
||||||
Files: nil,
|
Files: nil,
|
||||||
Glob: "../testdata/template/hello*",
|
Glob: "../testdata/template/hello*",
|
||||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
FileSystem: nil,
|
||||||
FuncMap: 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,
|
||||||
}
|
}
|
||||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||||
"name": "thinkerou",
|
"name": "thinkerou",
|
||||||
@ -526,10 +584,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderHTMLDebugPanics(t *testing.T) {
|
func TestRenderHTMLDebugPanics(t *testing.T) {
|
||||||
htmlRender := HTMLDebug{
|
htmlRender := HTMLDebug{
|
||||||
Files: nil,
|
Files: nil,
|
||||||
Glob: "",
|
Glob: "",
|
||||||
Delims: Delims{"{{", "}}"},
|
FileSystem: nil,
|
||||||
FuncMap: nil,
|
Patterns: nil,
|
||||||
|
Delims: Delims{"{{", "}}"},
|
||||||
|
FuncMap: nil,
|
||||||
}
|
}
|
||||||
assert.Panics(t, func() { htmlRender.Instance("", nil) })
|
assert.Panics(t, func() { htmlRender.Instance("", nil) })
|
||||||
}
|
}
|
||||||
@ -581,7 +641,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderWriteError(t *testing.T) {
|
func TestRenderWriteError(t *testing.T) {
|
||||||
data := []interface{}{"value1", "value2"}
|
data := []any{"value1", "value2"}
|
||||||
prefix := "my-prefix:"
|
prefix := "my-prefix:"
|
||||||
r := SecureJSON{Data: data, Prefix: prefix}
|
r := SecureJSON{Data: data, Prefix: prefix}
|
||||||
ew := &errorWriter{
|
ew := &errorWriter{
|
||||||
|
|||||||
@ -15,7 +15,7 @@ type TOML struct {
|
|||||||
Data any
|
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.
|
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
|
||||||
func (r TOML) Render(w http.ResponseWriter) error {
|
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.
|
// WriteContentType (TOML) writes TOML ContentType for response.
|
||||||
func (r TOML) WriteContentType(w http.ResponseWriter) {
|
func (r TOML) WriteContentType(w http.ResponseWriter) {
|
||||||
writeContentType(w, TOMLContentType)
|
writeContentType(w, tomlContentType)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ package render
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"github.com/goccy/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// YAML contains the given interface object.
|
// YAML contains the given interface object.
|
||||||
|
|||||||
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -16,6 +17,8 @@ const (
|
|||||||
defaultStatus = http.StatusOK
|
defaultStatus = http.StatusOK
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errHijackAlreadyWritten = errors.New("gin: response body already written")
|
||||||
|
|
||||||
// ResponseWriter ...
|
// ResponseWriter ...
|
||||||
type ResponseWriter interface {
|
type ResponseWriter interface {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
@ -106,6 +109,11 @@ func (w *responseWriter) Written() bool {
|
|||||||
|
|
||||||
// Hijack implements the http.Hijacker interface.
|
// Hijack implements the http.Hijacker interface.
|
||||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
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 {
|
if w.size < 0 {
|
||||||
w.size = 0
|
w.size = 0
|
||||||
}
|
}
|
||||||
@ -120,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
|||||||
// Flush implements the http.Flusher interface.
|
// Flush implements the http.Flusher interface.
|
||||||
func (w *responseWriter) Flush() {
|
func (w *responseWriter) Flush() {
|
||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@ -124,6 +126,132 @@ func TestResponseWriterHijack(t *testing.T) {
|
|||||||
w.Flush()
|
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) {
|
func TestResponseWriterFlush(t *testing.T) {
|
||||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
writer := &responseWriter{}
|
writer := &responseWriter{}
|
||||||
|
|||||||
@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
|
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.
|
||||||
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
|
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
|
||||||
// Gin by default uses: gin.Dir()
|
// Gin by default uses: gin.Dir()
|
||||||
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var MaxHandlers = 32
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SetMode(TestMode)
|
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.Static("/static", "."))
|
||||||
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@ -484,7 +484,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
|
|||||||
assert.Contains(t, w.Body.String(), "package gin")
|
assert.Contains(t, w.Body.String(), "package gin")
|
||||||
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
|
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
|
||||||
// else, Content-Type='text/x-go; charset=utf-8'
|
// 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.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, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
|
||||||
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
|
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
|
||||||
@ -764,7 +764,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
|
|||||||
// Test not found
|
// Test not found
|
||||||
router.Use(func(c *Context) {
|
router.Use(func(c *Context) {
|
||||||
// For not found routes full path is empty
|
// 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")
|
w := PerformRequest(router, http.MethodGet, "/not-found")
|
||||||
|
|||||||
@ -4,9 +4,16 @@
|
|||||||
|
|
||||||
package gin
|
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) {
|
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
|
||||||
r = New()
|
r = New()
|
||||||
c = r.allocateContext(0)
|
c = r.allocateContext(0)
|
||||||
@ -15,10 +22,39 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
|
|||||||
return
|
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) {
|
func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
|
||||||
c = r.allocateContext(r.maxParams)
|
c = r.allocateContext(r.maxParams)
|
||||||
c.reset()
|
c.reset()
|
||||||
c.writermem.reset(w)
|
c.writermem.reset(w)
|
||||||
return
|
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
2
testdata/test_file.txt
vendored
Normal 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.
|
||||||
31
tree.go
31
tree.go
@ -5,7 +5,6 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@ -14,12 +13,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"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.
|
// Param is a single URL parameter, consisting of a key and a value.
|
||||||
type Param struct {
|
type Param struct {
|
||||||
Key string
|
Key string
|
||||||
@ -85,16 +78,13 @@ func (n *node) addChild(child *node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func countParams(path string) uint16 {
|
func countParams(path string) uint16 {
|
||||||
var n uint16
|
colons := strings.Count(path, ":")
|
||||||
s := bytesconv.StringToBytes(path)
|
stars := strings.Count(path, "*")
|
||||||
n += uint16(bytes.Count(s, strColon))
|
return safeUint16(colons + stars)
|
||||||
n += uint16(bytes.Count(s, strStar))
|
|
||||||
return n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func countSections(path string) uint16 {
|
func countSections(path string) uint16 {
|
||||||
s := bytesconv.StringToBytes(path)
|
return safeUint16(strings.Count(path, "/"))
|
||||||
return uint16(bytes.Count(s, strSlash))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type nodeType uint8
|
type nodeType uint8
|
||||||
@ -234,7 +224,7 @@ walk:
|
|||||||
// Wildcard conflict
|
// Wildcard conflict
|
||||||
pathSeg := path
|
pathSeg := path
|
||||||
if n.nType != catchAll {
|
if n.nType != catchAll {
|
||||||
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
pathSeg, _, _ = strings.Cut(pathSeg, "/")
|
||||||
}
|
}
|
||||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||||
panic("'" + pathSeg +
|
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] == '/' {
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||||
pathSeg := ""
|
pathSeg := ""
|
||||||
if len(n.children) != 0 {
|
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 +
|
panic("catch-all wildcard '" + path +
|
||||||
"' in new path '" + fullPath +
|
"' in new path '" + fullPath +
|
||||||
@ -383,7 +373,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||||||
}
|
}
|
||||||
|
|
||||||
n.addChild(child)
|
n.addChild(child)
|
||||||
n.indices = string('/')
|
n.indices = "/"
|
||||||
n = child
|
n = child
|
||||||
n.priority++
|
n.priority++
|
||||||
|
|
||||||
@ -681,12 +671,7 @@ walk: // Outer loop for walking the tree
|
|||||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||||
const stackBufSize = 128
|
const stackBufSize = 128
|
||||||
|
|
||||||
// Use a static sized buffer on the stack in the common case.
|
buf := make([]byte, 0, max(stackBufSize, len(path)+1))
|
||||||
// If the path is too long, allocate a buffer on the heap instead.
|
|
||||||
buf := make([]byte, 0, stackBufSize)
|
|
||||||
if length := len(path) + 1; length > stackBufSize {
|
|
||||||
buf = make([]byte, 0, length)
|
|
||||||
}
|
|
||||||
|
|
||||||
ciPath := n.findCaseInsensitivePathRec(
|
ciPath := n.findCaseInsensitivePathRec(
|
||||||
path,
|
path,
|
||||||
|
|||||||
@ -481,7 +481,7 @@ func TestTreeDuplicatePath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//printChildren(tree, "")
|
// printChildren(tree, "")
|
||||||
|
|
||||||
checkRequests(t, tree, testRequests{
|
checkRequests(t, tree, testRequests{
|
||||||
{"/", false, "/", nil},
|
{"/", false, "/", nil},
|
||||||
@ -532,7 +532,7 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
|
|||||||
|
|
||||||
func TestTreeCatchMaxParams(t *testing.T) {
|
func TestTreeCatchMaxParams(t *testing.T) {
|
||||||
tree := &node{}
|
tree := &node{}
|
||||||
var route = "/cmd/*filepath"
|
route := "/cmd/*filepath"
|
||||||
tree.addRoute(route, fakeHandler(route))
|
tree.addRoute(route, fakeHandler(route))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,7 +692,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRedirectTrailingSlash(t *testing.T) {
|
func TestRedirectTrailingSlash(t *testing.T) {
|
||||||
var data = []struct {
|
data := []struct {
|
||||||
path string
|
path string
|
||||||
}{
|
}{
|
||||||
{"/hello/:name"},
|
{"/hello/:name"},
|
||||||
|
|||||||
25
utils.go
25
utils.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -18,6 +19,12 @@ import (
|
|||||||
// BindKey indicates a default bind key.
|
// BindKey indicates a default bind key.
|
||||||
const BindKey = "_gin-gonic/gin/bindkey"
|
const BindKey = "_gin-gonic/gin/bindkey"
|
||||||
|
|
||||||
|
// localhostIP indicates the default localhost IP address.
|
||||||
|
const localhostIP = "127.0.0.1"
|
||||||
|
|
||||||
|
// localhostIPv6 indicates the default localhost IPv6 address.
|
||||||
|
const localhostIPv6 = "::1"
|
||||||
|
|
||||||
// Bind is a helper function for given interface object and returns a Gin middleware.
|
// Bind is a helper function for given interface object and returns a Gin middleware.
|
||||||
func Bind(val any) HandlerFunc {
|
func Bind(val any) HandlerFunc {
|
||||||
value := reflect.ValueOf(val)
|
value := reflect.ValueOf(val)
|
||||||
@ -155,10 +162,26 @@ func resolveAddress(addr []string) string {
|
|||||||
|
|
||||||
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
||||||
func isASCII(s string) bool {
|
func isASCII(s string) bool {
|
||||||
for i := 0; i < len(s); i++ {
|
for i := range len(s) {
|
||||||
if s[i] > unicode.MaxASCII {
|
if s[i] > unicode.MaxASCII {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
|
||||||
|
func safeInt8(n int) int8 {
|
||||||
|
if n > math.MaxInt8 {
|
||||||
|
return math.MaxInt8
|
||||||
|
}
|
||||||
|
return int8(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
|
||||||
|
func safeUint16(n int) uint16 {
|
||||||
|
if n > math.MaxUint16 {
|
||||||
|
return math.MaxUint16
|
||||||
|
}
|
||||||
|
return uint16(n)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParseAccept(b *testing.B) {
|
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")
|
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +95,7 @@ func somefunction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJoinPaths(t *testing.T) {
|
func TestJoinPaths(t *testing.T) {
|
||||||
assert.Equal(t, "", joinPaths("", ""))
|
assert.Empty(t, joinPaths("", ""))
|
||||||
assert.Equal(t, "/", joinPaths("", "/"))
|
assert.Equal(t, "/", joinPaths("", "/"))
|
||||||
assert.Equal(t, "/a", joinPaths("/a", ""))
|
assert.Equal(t, "/a", joinPaths("/a", ""))
|
||||||
assert.Equal(t, "/a/", joinPaths("/a/", ""))
|
assert.Equal(t, "/a/", joinPaths("/a/", ""))
|
||||||
@ -148,3 +149,13 @@ func TestIsASCII(t *testing.T) {
|
|||||||
assert.True(t, isASCII("test"))
|
assert.True(t, isASCII("test"))
|
||||||
assert.False(t, isASCII("🧡💛💚💙💜"))
|
assert.False(t, isASCII("🧡💛💚💙💜"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSafeInt8(t *testing.T) {
|
||||||
|
assert.Equal(t, int8(100), safeInt8(100))
|
||||||
|
assert.Equal(t, int8(math.MaxInt8), safeInt8(int(math.MaxInt8)+123))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeUint16(t *testing.T) {
|
||||||
|
assert.Equal(t, uint16(100), safeUint16(100))
|
||||||
|
assert.Equal(t, uint16(math.MaxUint16), safeUint16(int(math.MaxUint16)+123))
|
||||||
|
}
|
||||||
|
|||||||
@ -5,4 +5,4 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
// Version is the current gin framework's version.
|
// Version is the current gin framework's version.
|
||||||
const Version = "v1.10.0"
|
const Version = "v1.11.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user