mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-11 14:11:46 +08:00
Compare commits
233 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3ffc99852 | ||
|
|
ecd26c8835 | ||
|
|
a749e4d33c | ||
|
|
65d1c470ec | ||
|
|
6d880724cc | ||
|
|
48667a2dd1 | ||
|
|
d4672219fc | ||
|
|
3e44fdc4d1 | ||
|
|
cb2b764cc8 | ||
|
|
a39670fb7b | ||
|
|
052d1a79aa | ||
|
|
ff00c01e67 | ||
|
|
73726dc606 | ||
|
|
e292e5caa7 | ||
|
|
ae3f524974 | ||
|
|
38534e2bf9 | ||
|
|
472d086af2 | ||
|
|
fb2583442c | ||
|
|
6f1d5fe3cd | ||
|
|
5c00df8afa | ||
|
|
db309081bc | ||
|
|
ba093d1947 | ||
|
|
1b414bd54e | ||
|
|
81dba46872 | ||
|
|
0c219e7902 | ||
|
|
00900fb3e1 | ||
|
|
5260de6a83 | ||
|
|
5f424ff6f6 | ||
|
|
216a4a7c28 | ||
|
|
f5c267d2f8 | ||
|
|
bf52b077c8 | ||
|
|
6e3ac82fa7 | ||
|
|
71cefce08e | ||
|
|
882f42b0ed | ||
|
|
488f8c3ffa | ||
|
|
8e07d37c63 | ||
|
|
d7776de7d4 | ||
|
|
e3118cc378 | ||
|
|
cad29c5e3f | ||
|
|
d9e5cdf9c6 | ||
|
|
53410d2e07 | ||
|
|
ac95fa6bbc | ||
|
|
192ac89eef | ||
|
|
b2b489dbf4 | ||
|
|
3ab698dc51 | ||
|
|
9914178584 | ||
|
|
915e4c90d2 | ||
|
|
26c3a62865 | ||
|
|
22c274c84b | ||
|
|
d1a15347b1 | ||
|
|
64a6ed9a41 | ||
|
|
19b877fa50 | ||
|
|
2a794cd0b0 | ||
|
|
b917b14ff9 | ||
|
|
fad706f121 | ||
|
|
f416d1e594 | ||
|
|
583db590ec | ||
|
|
af6e8b70b8 | ||
|
|
63dd3e60ca | ||
|
|
c358d5656d | ||
|
|
771dcc6476 | ||
|
|
52ecf029bd | ||
|
|
440eb14ab8 | ||
|
|
ecb3f7b5e2 | ||
|
|
e88fc8927a | ||
|
|
5fad976b37 | ||
|
|
93ff771e6d | ||
|
|
58135f06cf | ||
|
|
a85ef5ce4d | ||
|
|
fb27ef26c2 | ||
|
|
19c2d5c0d1 | ||
|
|
a9401cd238 | ||
|
|
d1bcabc7ee | ||
|
|
c3d5a28ed6 | ||
|
|
acc55e049e | ||
|
|
0c0e99d253 | ||
|
|
dceb61e6e7 | ||
|
|
5e5ff3ace4 | ||
|
|
2e22e50859 | ||
|
|
52f70cf18a | ||
|
|
87c207a140 | ||
|
|
c0048f645e | ||
|
|
38e7651192 | ||
|
|
c221133ee8 | ||
|
|
c3d1092b3b | ||
|
|
9968c4bf9d | ||
|
|
053e5765fd | ||
|
|
0d085ed9fe | ||
|
|
5dd833f1f2 | ||
|
|
48a5dca087 | ||
|
|
0bd10a84f9 | ||
|
|
4dec17afdf | ||
|
|
731374fb36 | ||
|
|
8ca975441f | ||
|
|
39858a0859 | ||
|
|
ed150e7254 | ||
|
|
234a6d4c00 | ||
|
|
df2753778e | ||
|
|
048f6fb884 | ||
|
|
61b67de522 | ||
|
|
f3a5e78719 | ||
|
|
414de60574 | ||
|
|
59e9d4a794 | ||
|
|
6a1d1218c3 | ||
|
|
7925414704 | ||
|
|
1bbbec0baf | ||
|
|
4dd00f81b1 | ||
|
|
6ad6205e9c | ||
|
|
7858527c8c | ||
|
|
cb000f570c | ||
|
|
2119046230 | ||
|
|
da372fc778 | ||
|
|
e198f6e859 | ||
|
|
cca98d2d26 | ||
|
|
9b1e3533e2 | ||
|
|
f9bd00a6b7 | ||
|
|
28172fa682 | ||
|
|
46150257b3 | ||
|
|
e7693e67c2 | ||
|
|
077a2f39c8 | ||
|
|
45b805f6d5 | ||
|
|
17d0b553ea | ||
|
|
42f93283cf | ||
|
|
32065bbd42 | ||
|
|
b987b6206f | ||
|
|
dab5944a7b | ||
|
|
9708475b3b | ||
|
|
e4c2a27624 | ||
|
|
a4ac275e07 | ||
|
|
ae5be7fcb7 | ||
|
|
57ec9e6036 | ||
|
|
ad4f436ae9 | ||
|
|
5826722a87 | ||
|
|
bdc1ad7987 | ||
|
|
545fd74379 | ||
|
|
a6287825c9 | ||
|
|
dbd8a25150 | ||
|
|
b7d6308bcc | ||
|
|
4bdcd9d0f1 | ||
|
|
76dd08d512 | ||
|
|
cf4775283e | ||
|
|
688a429d19 | ||
|
|
0a864884de | ||
|
|
dd33ff7938 | ||
|
|
77d70e5858 | ||
|
|
a9c5b36578 | ||
|
|
e30123ad73 | ||
|
|
3c12d2a80e | ||
|
|
61c2b1c28f | ||
|
|
41d8591eb1 | ||
|
|
848e1cdd0d | ||
|
|
c8af82af15 | ||
|
|
40725d85ba | ||
|
|
c4287b1300 | ||
|
|
8fb3136664 | ||
|
|
674522db91 | ||
|
|
8f7c340577 | ||
|
|
d00e6a5695 | ||
|
|
19f5a13fb4 | ||
|
|
fb09c825e8 | ||
|
|
3d8e288c64 | ||
|
|
2e2bd1f408 | ||
|
|
da67cc1b98 | ||
|
|
ef68fa032c | ||
|
|
b38c59de7f | ||
|
|
cf32d2dcf8 | ||
|
|
4714c2a9a3 | ||
|
|
7a1b655074 | ||
|
|
67c9d4ee11 | ||
|
|
bb82473103 | ||
|
|
255af882db | ||
|
|
71496abe68 | ||
|
|
0eb99493c2 | ||
|
|
afa0c31d97 | ||
|
|
56fccc39ec | ||
|
|
3319038418 | ||
|
|
49e9137c68 | ||
|
|
1b53a47790 | ||
|
|
3afff295a2 | ||
|
|
8763f33c65 | ||
|
|
e737e3e267 | ||
|
|
4ccfa7c275 | ||
|
|
90cf460269 | ||
|
|
ebe5e2a6bf | ||
|
|
733ee094fc | ||
|
|
a4baac6e5e | ||
|
|
1eb827240e | ||
|
|
3b28645dc9 | ||
|
|
c3c8620a7f | ||
|
|
3f818c3fa6 | ||
|
|
23d6961aeb | ||
|
|
e2e80f3347 | ||
|
|
e46bd52185 | ||
|
|
e8d34d053f | ||
|
|
02c1144f31 | ||
|
|
f875d87283 | ||
|
|
c8a3adc657 | ||
|
|
ea53388e6e | ||
|
|
9d11234efe | ||
|
|
647311aba2 | ||
|
|
299c6f30e3 | ||
|
|
b080116a7f | ||
|
|
ad740d508f | ||
|
|
f05f966a08 | ||
|
|
9d7c0e9e1a | ||
|
|
f2c861a24f | ||
|
|
28e57f58b1 | ||
|
|
3cb30679b5 | ||
|
|
cc4e11438c | ||
|
|
5f55c6a711 | ||
|
|
626d55b0c0 | ||
|
|
9c081de9cd | ||
|
|
64ead9e6bd | ||
|
|
4621b7ac98 | ||
|
|
334160bab7 | ||
|
|
24d67647cb | ||
|
|
e0d46ded6c | ||
|
|
4f339e6a35 | ||
|
|
36b0dede4b | ||
|
|
3f5b0afa2a | ||
|
|
a569ed8f26 | ||
|
|
6ca8ddb1ae | ||
|
|
40131af124 | ||
|
|
c677ccc40a | ||
|
|
7e298066ba | ||
|
|
3ac729dc4a | ||
|
|
8791c96960 | ||
|
|
b1c1e7b572 | ||
|
|
7d147928ee | ||
|
|
f5f5da8fa0 | ||
|
|
8dd088927a | ||
|
|
e60113dc95 | ||
|
|
490accf5d7 |
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:
|
||||
- Open your pull request against `master`
|
||||
- Your pull request should have no more than two commits, if not you should squash them.
|
||||
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
|
||||
- You should add/modify tests to cover your proposed code changes.
|
||||
- If your pull request contains a new feature, please document it on the README.
|
||||
# Pull Request Checklist
|
||||
|
||||
Please ensure your pull request meets the following requirements:
|
||||
|
||||
- [ ] Open your pull request against the `master` branch.
|
||||
- [ ] All tests pass in available continuous integration systems (e.g., GitHub Actions).
|
||||
- [ ] Tests are added or modified as needed to cover code changes.
|
||||
- [ ] If the pull request introduces a new feature, the feature is documented in the `docs/doc.md`.
|
||||
|
||||
Thank you for contributing!
|
||||
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@ -1,10 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: daily
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -33,11 +33,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -46,4 +46,4 @@ jobs:
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
33
.github/workflows/gin.yml
vendored
33
.github/workflows/gin.yml
vendored
@ -16,27 +16,32 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
go-version: "^1"
|
||||
- name: Setup golangci-lint
|
||||
uses: golangci/golangci-lint-action@v5
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v1.56.2
|
||||
version: v2.11
|
||||
args: --verbose
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go: ["1.18", "1.19", "1.20", "1.21", "1.22"]
|
||||
go: ["1.25", "1.26"]
|
||||
test-tags:
|
||||
["", "-tags nomsgpack", '-tags "sonic avx"', "-tags go_json", "-race"]
|
||||
[
|
||||
"",
|
||||
"-tags nomsgpack",
|
||||
'--ldflags="-checklinkname=0" -tags sonic',
|
||||
"-tags go_json",
|
||||
"-race",
|
||||
]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
go-build: ~/.cache/go-build
|
||||
@ -50,17 +55,17 @@ jobs:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
${{ matrix.go-build }}
|
||||
@ -73,10 +78,6 @@ jobs:
|
||||
run: make test
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
|
||||
|
||||
- name: Format
|
||||
if: matrix.go-version == '1.22.x'
|
||||
run: diff -u <(echo -n) <(gofmt -d .)
|
||||
|
||||
11
.github/workflows/goreleaser.yml
vendored
11
.github/workflows/goreleaser.yml
vendored
@ -13,15 +13,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1"
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
@ -29,3 +29,8 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Go module reindex (pkg.go.dev)
|
||||
run: |
|
||||
echo "Triggering Go module reindex at proxy.golang.org"
|
||||
curl -sSf "https://proxy.golang.org/github.com/${GITHUB_REPOSITORY,,}/@v/${GITHUB_REF_NAME}.info"
|
||||
|
||||
56
.github/workflows/trivy-scan.yml
vendored
Normal file
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.35.0
|
||||
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.35.0
|
||||
if: always()
|
||||
with:
|
||||
scan-type: "fs"
|
||||
scan-ref: "."
|
||||
scanners: "vuln,secret,misconfig"
|
||||
format: "table"
|
||||
severity: "CRITICAL,HIGH,MEDIUM"
|
||||
ignore-unfixed: true
|
||||
exit-code: "1"
|
||||
106
.golangci.yml
106
.golangci.yml
@ -1,57 +1,75 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- gci
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
- revive
|
||||
- testifylint
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
|
||||
linters-settings:
|
||||
gosec:
|
||||
# To select a subset of rules to run.
|
||||
# Available rules: https://github.com/securego/gosec#available-rules
|
||||
# Default: [] - means include all rules
|
||||
includes:
|
||||
- G102
|
||||
- G106
|
||||
- G108
|
||||
- G109
|
||||
- G111
|
||||
- G112
|
||||
- G201
|
||||
- G203
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- structcheck
|
||||
- unused
|
||||
text: "`data` is unused"
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA1019:"
|
||||
- linters:
|
||||
- revive
|
||||
text: "var-naming:"
|
||||
- linters:
|
||||
- revive
|
||||
text: "exported:"
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec # security is not make sense in tests
|
||||
- linters:
|
||||
- revive
|
||||
path: _test\.go
|
||||
settings:
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
perfsprint:
|
||||
int-conversion: true
|
||||
err-error: true
|
||||
errorf: true
|
||||
sprintf1: true
|
||||
strconcat: true
|
||||
testifylint:
|
||||
enable-all: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- structcheck
|
||||
- unused
|
||||
text: '`data` is unused'
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: 'SA1019:'
|
||||
- linters:
|
||||
- revive
|
||||
text: 'var-naming:'
|
||||
- linters:
|
||||
- revive
|
||||
text: 'exported:'
|
||||
- linters:
|
||||
- gosec
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- revive
|
||||
path: _test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- gin.go
|
||||
|
||||
406
AUTHORS.md
406
AUTHORS.md
@ -1,406 +0,0 @@
|
||||
List of all the awesome people working to make Gin the best Web Framework in Go.
|
||||
|
||||
## gin 1.x series authors
|
||||
|
||||
**Gin Core Team:** Bo-Yi Wu (@appleboy), thinkerou (@thinkerou), Javier Provecho (@javierprovecho)
|
||||
|
||||
## gin 0.x series authors
|
||||
|
||||
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
||||
|
||||
------
|
||||
|
||||
People and companies, who have contributed, in alphabetical order.
|
||||
|
||||
- 178inaba <178inaba@users.noreply.github.com>
|
||||
- A. F <hello@clivern.com>
|
||||
- ABHISHEK SONI <abhishek.rocks26@gmail.com>
|
||||
- Abhishek Chanda <achanda@users.noreply.github.com>
|
||||
- Abner Chen <houjunchen@gmail.com>
|
||||
- AcoNCodes <acongame@gmail.com>
|
||||
- Adam Dratwinski <adam.dratwinski@gmail.com>
|
||||
- Adam Mckaig <adam.mckaig@gmail.com>
|
||||
- Adam Zielinski <MusicAdam@users.noreply.github.com>
|
||||
- Adonis <donileo@gmail.com>
|
||||
- Alan Wang <azzwacb9001@126.com>
|
||||
- Albin Gilles <gilles.albin@gmail.com>
|
||||
- Aleksandr Didenko <aa.didenko@yandex.ru>
|
||||
- Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
|
||||
- Alex <AWulkan@users.noreply.github.com>
|
||||
- Alexander <alexanderchenmh@gmail.com>
|
||||
- Alexander Lokhman <alex.lokhman@gmail.com>
|
||||
- Alexander Melentyev <55826637+alexander-melentyev@users.noreply.github.com>
|
||||
- Alexander Nyquist <nyquist.alexander@gmail.com>
|
||||
- Allen Ren <kulong0105@gmail.com>
|
||||
- AllinGo <tanhp@outlook.com>
|
||||
- Ammar Bandukwala <ammar@ammar.io>
|
||||
- An Xiao (Luffy) <hac@zju.edu.cn>
|
||||
- Andre Dublin <81dublin@gmail.com>
|
||||
- Andrew Szeto <github@jabagawee.com>
|
||||
- Andrey Abramov <andreyabramov.aaa@gmail.com>
|
||||
- Andrey Nering <andrey.nering@gmail.com>
|
||||
- Andrey Smirnov <Smirnov.Andrey@gmail.com>
|
||||
- Andrii Bubis <firstrow@gmail.com>
|
||||
- André Bazaglia <bazaglia@users.noreply.github.com>
|
||||
- Andy Pan <panjf2000@gmail.com>
|
||||
- Antoine GIRARD <sapk@users.noreply.github.com>
|
||||
- Anup Kumar Panwar <1anuppanwar@gmail.com>
|
||||
- Aravinth Sundaram <gosh.aravind@gmail.com>
|
||||
- Artem <horechek@gmail.com>
|
||||
- Ashwani <ashwanisharma686@gmail.com>
|
||||
- Aurelien Regat-Barrel <arb@cyberkarma.net>
|
||||
- Austin Heap <me@austinheap.com>
|
||||
- Barnabus <jbampton@users.noreply.github.com>
|
||||
- Bo-Yi Wu <appleboy.tw@gmail.com>
|
||||
- Boris Borshevsky <BorisBorshevsky@gmail.com>
|
||||
- Boyi Wu <p581581@gmail.com>
|
||||
- BradyBromley <51128276+BradyBromley@users.noreply.github.com>
|
||||
- Brendan Fosberry <brendan@shopkeep.com>
|
||||
- Brian Wigginton <brianwigginton@gmail.com>
|
||||
- Carlos Eduardo <carlosedp@gmail.com>
|
||||
- Chad Russell <chaddouglasrussell@gmail.com>
|
||||
- Charles <cxjava@gmail.com>
|
||||
- Christian Muehlhaeuser <muesli@gmail.com>
|
||||
- Christian Persson <saser@live.se>
|
||||
- Christopher Harrington <ironiridis@gmail.com>
|
||||
- Damon Zhao <yijun.zhao@outlook.com>
|
||||
- Dan Markham <dmarkham@gmail.com>
|
||||
- Dang Nguyen <hoangdang.me@gmail.com>
|
||||
- Daniel Krom <kromdan@gmail.com>
|
||||
- Daniel M. Lambea <dmlambea@gmail.com>
|
||||
- Danieliu <liudanking@gmail.com>
|
||||
- David Irvine <aviddiviner@gmail.com>
|
||||
- David Zhang <crispgm@gmail.com>
|
||||
- Davor Kapsa <dvrkps@users.noreply.github.com>
|
||||
- DeathKing <DeathKing@users.noreply.github.com>
|
||||
- Dennis Cho <47404603+forest747@users.noreply.github.com>
|
||||
- Dmitry Dorogin <dmirogin@ya.ru>
|
||||
- Dmitry Kutakov <vkd.castle@gmail.com>
|
||||
- Dmitry Sedykh <dmitrys@d3h.local>
|
||||
- Don2Quixote <35610661+Don2Quixote@users.noreply.github.com>
|
||||
- Donn Pebe <iam@donnpebe.com>
|
||||
- Dustin Decker <dustindecker@protonmail.com>
|
||||
- Eason Lin <easonlin404@gmail.com>
|
||||
- Edward Betts <edward@4angle.com>
|
||||
- Egor Seredin <4819888+agmt@users.noreply.github.com>
|
||||
- Emmanuel Goh <emmanuel@visenze.com>
|
||||
- Equim <sayaka@ekyu.moe>
|
||||
- Eren A. Akyol <eren@redmc.me>
|
||||
- Eric_Lee <xplzv@126.com>
|
||||
- Erik Bender <erik.bender@develerik.dev>
|
||||
- Ethan Kan <ethankan@neoplot.com>
|
||||
- Evgeny Persienko <e.persienko@office.ngs.ru>
|
||||
- Faisal Alam <ifaisalalam@gmail.com>
|
||||
- Fareed Dudhia <fareeddudhia@googlemail.com>
|
||||
- Filip Figiel <figiel.filip@gmail.com>
|
||||
- Florian Polster <couchpolster@icqmail.com>
|
||||
- Frank Bille <github@frankbille.dk>
|
||||
- Franz Bettag <franz@bett.ag>
|
||||
- Ganlv <ganlvtech@users.noreply.github.com>
|
||||
- Gaozhen Ying <yinggaozhen@hotmail.com>
|
||||
- George Gabolaev <gabolaev98@gmail.com>
|
||||
- George Kirilenko <necryin@users.noreply.github.com>
|
||||
- Georges Varouchas <georges.varouchas@gmail.com>
|
||||
- Gordon Tyler <gordon@doxxx.net>
|
||||
- Harindu Perera <harinduenator@gmail.com>
|
||||
- Helios <674876158@qq.com>
|
||||
- Henry Kwan <piengeng@users.noreply.github.com>
|
||||
- Henry Yee <henry@yearning.io>
|
||||
- Himanshu Mishra <OrkoHunter@users.noreply.github.com>
|
||||
- Hiroyuki Tanaka <h.tanaka.0325@gmail.com>
|
||||
- Ibraheem Ahmed <ibrah1440@gmail.com>
|
||||
- Ignacio Galindo <joiggama@gmail.com>
|
||||
- Igor H. Vieira <zignd.igor@gmail.com>
|
||||
- Ildar1111 <54001462+Ildar1111@users.noreply.github.com>
|
||||
- Iskander (Alex) Sharipov <iskander.sharipov@intel.com>
|
||||
- Ismail Gjevori <isgjevori@protonmail.com>
|
||||
- Ivan Chen <allenivan@gmail.com>
|
||||
- JINNOUCHI Yasushi <delphinus@remora.cx>
|
||||
- James Pettyjohn <japettyjohn@users.noreply.github.com>
|
||||
- Jamie Stackhouse <jamie.stackhouse@redspace.com>
|
||||
- Jason Lee <jawc@hotmail.com>
|
||||
- Javier Provecho <j.provecho@dartekstudios.com>
|
||||
- Javier Provecho <javier.provecho@bq.com>
|
||||
- Javier Provecho <javiertitan@gmail.com>
|
||||
- Javier Provecho Fernandez <j.provecho@dartekstudios.com>
|
||||
- Javier Provecho Fernandez <javiertitan@gmail.com>
|
||||
- Jean-Christophe Lebreton <jclebreton@gmail.com>
|
||||
- Jeff <laojianzi1994@gmail.com>
|
||||
- Jeremy Loy <jeremy.b.loy@icloud.com>
|
||||
- Jim Filippou <p3160253@aueb.gr>
|
||||
- Jimmy Pettersson <jimmy@expertmaker.com>
|
||||
- John Bampton <jbampton@users.noreply.github.com>
|
||||
- Johnny Dallas <johnnydallas0308@gmail.com>
|
||||
- Johnny Dallas <theonlyjohnny@theonlyjohnny.sh>
|
||||
- Jonathan (JC) Chen <jc@dijonkitchen.org>
|
||||
- Josep Jesus Bigorra Algaba <42377845+averageflow@users.noreply.github.com>
|
||||
- Josh Horowitz <joshua.m.horowitz@gmail.com>
|
||||
- Joshua Loper <josh.el3@gmail.com>
|
||||
- Julien Schmidt <github@julienschmidt.com>
|
||||
- Jun Kimura <jksmphone@gmail.com>
|
||||
- Justin Beckwith <justin.beckwith@gmail.com>
|
||||
- Justin Israel <justinisrael@gmail.com>
|
||||
- Justin Mayhew <mayhew@live.ca>
|
||||
- Jérôme Laforge <jerome-laforge@users.noreply.github.com>
|
||||
- Kacper Bąk <56700396+53jk1@users.noreply.github.com>
|
||||
- Kamron Batman <kamronbatman@users.noreply.github.com>
|
||||
- Kane Rogers <kane@cleanstream.com.au>
|
||||
- Kaushik Neelichetty <kaushikneelichetty6132@gmail.com>
|
||||
- Keiji Yoshida <yoshida.keiji.84@gmail.com>
|
||||
- Kel Cecil <kel.cecil@listhub.com>
|
||||
- Kevin Mulvey <kmulvey@linux.com>
|
||||
- Kevin Zhu <ipandtcp@gmail.com>
|
||||
- Kirill Motkov <motkov.kirill@gmail.com>
|
||||
- Klemen Sever <ksever@student.42.fr>
|
||||
- Kristoffer A. Iversen <kristoffer.a.iversen@gmail.com>
|
||||
- Krzysztof Szafrański <k.p.szafranski@gmail.com>
|
||||
- Kumar McMillan <kumar.mcmillan@gmail.com>
|
||||
- Kyle Mcgill <email@kylescottmcgill.com>
|
||||
- Lanco <35420416+lancoLiu@users.noreply.github.com>
|
||||
- Levi Olson <olson.levi@gmail.com>
|
||||
- Lin Kao-Yuan <mosdeo@gmail.com>
|
||||
- Linus Unnebäck <linus@folkdatorn.se>
|
||||
- Lucas Clemente <lucas@clemente.io>
|
||||
- Ludwig Valda Vasquez <bredov@gmail.com>
|
||||
- Luis GG <lggomez@users.noreply.github.com>
|
||||
- MW Lim <williamchange@gmail.com>
|
||||
- Maksimov Sergey <konjoot@gmail.com>
|
||||
- Manjusaka <lizheao940510@gmail.com>
|
||||
- Manu MA <manu.mtza@gmail.com>
|
||||
- Manu MA <manu.valladolid@gmail.com>
|
||||
- Manu Mtz-Almeida <manu.valladolid@gmail.com>
|
||||
- Manu Mtz.-Almeida <manu.valladolid@gmail.com>
|
||||
- Manuel Alonso <manuelalonso@invisionapp.com>
|
||||
- Mara Kim <hacker.root@gmail.com>
|
||||
- Mario Kostelac <mario@intercom.io>
|
||||
- Martin Karlsch <martin@karlsch.com>
|
||||
- Matt Newberry <mnewberry@opentable.com>
|
||||
- Matt Williams <gh@mattyw.net>
|
||||
- Matthieu MOREL <mmorel-35@users.noreply.github.com>
|
||||
- Max Hilbrunner <mhilbrunner@users.noreply.github.com>
|
||||
- Maxime Soulé <btik-git@scoubidou.com>
|
||||
- MetalBreaker <johnymichelson@gmail.com>
|
||||
- Michael Puncel <mpuncel@squareup.com>
|
||||
- MichaelDeSteven <51652084+MichaelDeSteven@users.noreply.github.com>
|
||||
- Mike <38686456+icy4ever@users.noreply.github.com>
|
||||
- Mike Stipicevic <mst@ableton.com>
|
||||
- Miki Tebeka <miki.tebeka@gmail.com>
|
||||
- Miles <MilesLin@users.noreply.github.com>
|
||||
- Mirza Ceric <mirza.ceric@b2match.com>
|
||||
- Mykyta Semenistyi <nikeiwe@gmail.com>
|
||||
- Naoki Takano <honten@tinkermode.com>
|
||||
- Ngalim Siregar <ngalim.siregar@gmail.com>
|
||||
- Ni Hao <supernihaooo@qq.com>
|
||||
- Nick Gerakines <nick@gerakines.net>
|
||||
- Nikifor Seryakov <nikandfor@gmail.com>
|
||||
- Notealot <714804968@qq.com>
|
||||
- Olivier Mengué <dolmen@cpan.org>
|
||||
- Olivier Robardet <orobardet@users.noreply.github.com>
|
||||
- Pablo Moncada <pablo.moncada@bq.com>
|
||||
- Pablo Moncada <pmoncadaisla@gmail.com>
|
||||
- Panmax <967168@qq.com>
|
||||
- Peperoncino <2wua4nlyi@gmail.com>
|
||||
- Philipp Meinen <philipp@bind.ch>
|
||||
- Pierre Massat <pierre@massat.io>
|
||||
- Qt <golang.chen@gmail.com>
|
||||
- Quentin ROYER <aydendevg@gmail.com>
|
||||
- README Bot <35302948+codetriage-readme-bot@users.noreply.github.com>
|
||||
- Rafal Zajac <rzajac@gmail.com>
|
||||
- Rahul Datta Roy <rahuldroy@users.noreply.github.com>
|
||||
- Rajiv Kilaparti <rajivk085@gmail.com>
|
||||
- Raphael Gavache <raphael.gavache@datadoghq.com>
|
||||
- Ray Rodriguez <rayrod2030@gmail.com>
|
||||
- Regner Blok-Andersen <shadowdf@gmail.com>
|
||||
- Remco <remco@dutchcoders.io>
|
||||
- Rex Lee(李俊) <duguying2008@gmail.com>
|
||||
- Richard Lee <dlackty@gmail.com>
|
||||
- Riverside <wangyb65@gmail.com>
|
||||
- Robert Wilkinson <wilkinson.robert.a@gmail.com>
|
||||
- Rogier Lommers <rogier@lommers.org>
|
||||
- Rohan Pai <me@rohanpai.com>
|
||||
- Romain Beuque <rbeuque74@gmail.com>
|
||||
- Roman Belyakovsky <ihryamzik@gmail.com>
|
||||
- Roman Zaynetdinov <627197+zaynetro@users.noreply.github.com>
|
||||
- Roman Zaynetdinov <roman.zaynetdinov@lekane.com>
|
||||
- Ronald Petty <ronald.petty@rx-m.com>
|
||||
- Ross Wolf <31489089+rw-access@users.noreply.github.com>
|
||||
- Roy Lou <roylou@gmail.com>
|
||||
- Rubi <14269809+codenoid@users.noreply.github.com>
|
||||
- Ryan <46182144+ryanker@users.noreply.github.com>
|
||||
- Ryan J. Yoder <me@ryanjyoder.com>
|
||||
- SRK.Lyu <superalsrk@gmail.com>
|
||||
- Sai <sairoutine@gmail.com>
|
||||
- Samuel Abreu <sdepaula@gmail.com>
|
||||
- Santhosh Kumar <santhoshkumarr1096@gmail.com>
|
||||
- Sasha Melentyev <sasha@melentyev.io>
|
||||
- Sasha Myasoedov <msoedov@gmail.com>
|
||||
- Segev Finer <segev208@gmail.com>
|
||||
- Sergey Egorov <egorovhome@gmail.com>
|
||||
- Sergey Fedchenko <seregayoga@bk.ru>
|
||||
- Sergey Gonimar <sergey.gonimar@gmail.com>
|
||||
- Sergey Ponomarev <me@sergey-ponomarev.ru>
|
||||
- Serica <943914044@qq.com>
|
||||
- Shamus Taylor <Shamus03@me.com>
|
||||
- Shilin Wang <jarvisfironman@gmail.com>
|
||||
- Shuo <openset.wang@gmail.com>
|
||||
- Skuli Oskarsson <skuli@codeiak.io>
|
||||
- Snawoot <vladislav-ex-github@vm-0.com>
|
||||
- Sridhar Ratnakumar <srid@srid.ca>
|
||||
- Steeve Chailloux <steeve@chaahk.com>
|
||||
- Sudhir Mishra <sudhirxps@gmail.com>
|
||||
- Suhas Karanth <sudo-suhas@users.noreply.github.com>
|
||||
- TaeJun Park <miking38@gmail.com>
|
||||
- Tatsuya Hoshino <tatsuya7.hoshino7@gmail.com>
|
||||
- Tevic <tevic.tt@gmail.com>
|
||||
- Tevin Jeffrey <tev.jeffrey@gmail.com>
|
||||
- The Gitter Badger <badger@gitter.im>
|
||||
- Thibault Jamet <tjamet@users.noreply.github.com>
|
||||
- Thomas Boerger <thomas@webhippie.de>
|
||||
- Thomas Schaffer <loopfz@gmail.com>
|
||||
- Tommy Chu <tommychu2256@gmail.com>
|
||||
- Tudor Roman <tudurom@gmail.com>
|
||||
- Uwe Dauernheim <djui@users.noreply.github.com>
|
||||
- Valentine Oragbakosi <valentine13400@gmail.com>
|
||||
- Vas N <pnvasanth@users.noreply.github.com>
|
||||
- Vasilyuk Vasiliy <By-Vasiliy@users.noreply.github.com>
|
||||
- Victor Castell <victor@victorcastell.com>
|
||||
- Vince Yuan <vince.yuan@gmail.com>
|
||||
- Vyacheslav Dubinin <vyacheslav.dubinin@gmail.com>
|
||||
- Waynerv <ampedee@gmail.com>
|
||||
- Weilin Shi <934587911@qq.com>
|
||||
- Xudong Cai <fifsky@gmail.com>
|
||||
- Yasuhiro Matsumoto <mattn.jp@gmail.com>
|
||||
- Yehezkiel Syamsuhadi <ybs@ybs.im>
|
||||
- Yoshiki Nakagawa <yyoshiki41@gmail.com>
|
||||
- Yoshiyuki Kinjo <yskkin+github@gmail.com>
|
||||
- Yue Yang <g1enyy0ung@gmail.com>
|
||||
- ZYunH <zyunhjob@163.com>
|
||||
- Zach Newburgh <zach.newburgh@gmail.com>
|
||||
- Zasda Yusuf Mikail <zasdaym@gmail.com>
|
||||
- ZhangYunHao <zyunhjob@163.com>
|
||||
- ZhiFeng Hu <hufeng1987@gmail.com>
|
||||
- Zhu Xi <zhuxi910511@163.com>
|
||||
- a2tt <usera2tt@gmail.com>
|
||||
- ahuigo <1781999+ahuigo@users.noreply.github.com>
|
||||
- ali <anio@users.noreply.github.com>
|
||||
- aljun <salameryy@163.com>
|
||||
- andrea <crypto.andrea@protonmail.ch>
|
||||
- andriikushch <andrii.kushch@gmail.com>
|
||||
- anoty <anjunyou@foxmail.com>
|
||||
- awkj <hzzbiu@gmail.com>
|
||||
- axiaoxin <254606826@qq.com>
|
||||
- bbiao <bbbiao@gmail.com>
|
||||
- bestgopher <84328409@qq.com>
|
||||
- betahu <zhong.wenhuang@foxmail.com>
|
||||
- bigwheel <k.bigwheel+eng@gmail.com>
|
||||
- bn4t <17193640+bn4t@users.noreply.github.com>
|
||||
- bullgare <bullgare@gmail.com>
|
||||
- chainhelen <chainhelen@gmail.com>
|
||||
- chenyang929 <chenyang929code@gmail.com>
|
||||
- chriswhelix <chris.williams@helix.com>
|
||||
- collinmsn <4130944@qq.com>
|
||||
- cssivision <cssivision@gmail.com>
|
||||
- danielalves <alves.lopes.dan@gmail.com>
|
||||
- delphinus <delphinus@remora.cx>
|
||||
- dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||
- dickeyxxx <jeff@dickeyxxx.com>
|
||||
- edebernis <emeric.debernis@gmail.com>
|
||||
- error10 <error@ioerror.us>
|
||||
- esplo <esplo@users.noreply.github.com>
|
||||
- eudore <30709860+eudore@users.noreply.github.com>
|
||||
- ffhelicopter <32922889+ffhelicopter@users.noreply.github.com>
|
||||
- filikos <11477309+filikos@users.noreply.github.com>
|
||||
- forging2012 <forging2012@users.noreply.github.com>
|
||||
- goqihoo <goqihoo@gmail.com>
|
||||
- grapeVine <treeui.old@gmail.com>
|
||||
- guonaihong <guonaihong@qq.com>
|
||||
- heige <daheige@users.noreply.github.com>
|
||||
- heige <zhuwei313@hotmail.com>
|
||||
- hellojukay <hellojukay@163.com>
|
||||
- henrylee2cn <henrylee2cn@gmail.com>
|
||||
- htobenothing <htobenothing@gmail.com>
|
||||
- iamhesir <78344375+iamhesir@users.noreply.github.com>
|
||||
- ijaa <kailiu2013@gmail.com>
|
||||
- ishanray <ishan.iipm@gmail.com>
|
||||
- ishanray <ishanray@users.noreply.github.com>
|
||||
- itcloudy <272685110@qq.com>
|
||||
- jarodsong6 <jarodsong6@gmail.com>
|
||||
- jasonrhansen <jasonrodneyhansen@gmail.com>
|
||||
- jincheng9 <perfume0607@gmail.com>
|
||||
- joeADSP <75027008+joeADSP@users.noreply.github.com>
|
||||
- junfengye <junfeng.yejf@gmail.com>
|
||||
- kaiiak <aNxFi37X@outlook.com>
|
||||
- kebo <kevinke2020@outlook.com>
|
||||
- keke <19yamashita15@gmail.com>
|
||||
- kishor kunal raj <68464660+kishorkunal-raj@users.noreply.github.com>
|
||||
- kyledinh <kyledinh@gmail.com>
|
||||
- lantw44 <lantw44@gmail.com>
|
||||
- likakuli <1154584512@qq.com>
|
||||
- linfangrong <linfangrong.liuxin@qq.com>
|
||||
- linzi <873804682@qq.com>
|
||||
- llgoer <yanghuxiao@vip.qq.com>
|
||||
- long-road <13412081338@163.com>
|
||||
- mbesancon <mathieu.besancon@gmail.com>
|
||||
- mehdy <mehdy.khoshnoody@gmail.com>
|
||||
- metal A-wing <freedom.awing.777@gmail.com>
|
||||
- micanzhang <micanzhang@gmail.com>
|
||||
- minarc <ragnhildmowinckel@gmail.com>
|
||||
- mllu <mornlyn@gmail.com>
|
||||
- mopemoepe <yutaka.matsubara@gmail.com>
|
||||
- msoedov <msoedov@gmail.com>
|
||||
- mstmdev <mstmdev@gmail.com>
|
||||
- novaeye <fcoffee@gmail.com>
|
||||
- olebedev <oolebedev@gmail.com>
|
||||
- phithon <phith0n@users.noreply.github.com>
|
||||
- pjgg <pablo.gonzalez.granados@gmail.com>
|
||||
- qm012 <67568757+qm012@users.noreply.github.com>
|
||||
- raymonder jin <rayjingithub@gmail.com>
|
||||
- rns <ruslan.shvedov@gmail.com>
|
||||
- root@andrea:~# <crypto.andrea@protonmail.ch>
|
||||
- sekky0905 <20237968+sekky0905@users.noreply.github.com>
|
||||
- senhtry <w169q169@gmail.com>
|
||||
- shadrus <shadrus@gmail.com>
|
||||
- silasb <silas.baronda@gmail.com>
|
||||
- solos <lxl1217@gmail.com>
|
||||
- songjiayang <songjiayang@users.noreply.github.com>
|
||||
- sope <shenshouer@163.com>
|
||||
- srt180 <30768686+srt180@users.noreply.github.com>
|
||||
- stackerzzq <foo_stacker@yeah.net>
|
||||
- sunshineplan <sunshineplan@users.noreply.github.com>
|
||||
- syssam <s.y.s.sam.sys@gmail.com>
|
||||
- techjanitor <puntme@gmail.com>
|
||||
- techjanitor <techjanitor@users.noreply.github.com>
|
||||
- thinkerou <thinkerou@gmail.com>
|
||||
- thinkgo <49174849+thinkgos@users.noreply.github.com>
|
||||
- tsirolnik <tsirolnik@users.noreply.github.com>
|
||||
- tyltr <31768692+tylitianrui@users.noreply.github.com>
|
||||
- vinhha96 <anhvinha1@gmail.com>
|
||||
- voidman <retmain@foxmail.com>
|
||||
- vz <vzvway@gmail.com>
|
||||
- wei <wei840222@gmail.com>
|
||||
- weibaohui <weibaohui@yeah.net>
|
||||
- whirosan <whirosan@users.noreply.github.com>
|
||||
- willnewrelic <will@newrelic.com>
|
||||
- wssccc <wssccc@qq.com>
|
||||
- wuhuizuo <wuhuizuo@126.com>
|
||||
- xyb <xyb4638@gmail.com>
|
||||
- y-yagi <yuuji.yaginuma@gmail.com>
|
||||
- yiranzai <wuqingdzx@gmail.com>
|
||||
- youzeliang <youzel@126.com>
|
||||
- yugu <chenzilong_1227@foxmail.com>
|
||||
- yuyabe <yuyabee@gmail.com>
|
||||
- zebozhuang <zebozhuang@163.com>
|
||||
- zero11-0203 <93071220+zero11-0203@users.noreply.github.com>
|
||||
- zesani <7sin@outlook.co.th>
|
||||
- zhanweidu <zhanweidu@163.com>
|
||||
- zhing <zqwillseven@gmail.com>
|
||||
- ziheng <zihenglv@gmail.com>
|
||||
- zzjin <zzjin@users.noreply.github.com>
|
||||
- 森 優太 <59682979+uta-mori@users.noreply.github.com>
|
||||
- 杰哥 <858806258@qq.com>
|
||||
- 涛叔 <hi@taoshu.in>
|
||||
- 市民233 <mengrenxiong@gmail.com>
|
||||
- 尹宝强 <wmdandme@gmail.com>
|
||||
- 梦溪笔谈 <loongmxbt@gmail.com>
|
||||
- 飞雪无情 <ls8707@gmail.com>
|
||||
- 寻寻觅觅的Gopher <zoujh99@qq.com>
|
||||
897
BENCHMARKS.md
897
BENCHMARKS.md
@ -1,666 +1,291 @@
|
||||
# Gin Benchmark Report
|
||||
|
||||
# Benchmark System
|
||||
|
||||
**VM HOST:** Travis
|
||||
**Machine:** Ubuntu 16.04.6 LTS x64
|
||||
**Date:** May 04th, 2020
|
||||
**Version:** Gin v1.6.3
|
||||
**Go Version:** 1.14.2 linux/amd64
|
||||
**Machine:** Apple M4 Pro
|
||||
**OS:** macOS (Darwin 25.3.0), arm64
|
||||
**Date:** March 15th, 2026
|
||||
**Gin Version:** v1.12.0
|
||||
**Go Version:** 1.25.8 darwin/arm64
|
||||
**Source:** [Go HTTP Router Benchmark](https://github.com/gin-gonic/go-http-routing-benchmark)
|
||||
**Result:** [See the gist](https://gist.github.com/appleboy/b5f2ecfaf50824ae9c64dcfb9165ae5e) or [Travis result](https://travis-ci.org/github/gin-gonic/go-http-routing-benchmark/jobs/682947061)
|
||||
|
||||
## Static Routes: 157
|
||||
---
|
||||
|
||||
```sh
|
||||
Gin: 34936 Bytes
|
||||
## Table of Contents
|
||||
|
||||
HttpServeMux: 14512 Bytes
|
||||
Ace: 30680 Bytes
|
||||
Aero: 34536 Bytes
|
||||
Bear: 30456 Bytes
|
||||
Beego: 98456 Bytes
|
||||
Bone: 40224 Bytes
|
||||
Chi: 83608 Bytes
|
||||
Denco: 10216 Bytes
|
||||
Echo: 80328 Bytes
|
||||
GocraftWeb: 55288 Bytes
|
||||
Goji: 29744 Bytes
|
||||
Gojiv2: 105840 Bytes
|
||||
GoJsonRest: 137496 Bytes
|
||||
GoRestful: 816936 Bytes
|
||||
GorillaMux: 585632 Bytes
|
||||
GowwwRouter: 24968 Bytes
|
||||
HttpRouter: 21712 Bytes
|
||||
HttpTreeMux: 73448 Bytes
|
||||
Kocha: 115472 Bytes
|
||||
LARS: 30640 Bytes
|
||||
Macaron: 38592 Bytes
|
||||
Martini: 310864 Bytes
|
||||
Pat: 19696 Bytes
|
||||
Possum: 89920 Bytes
|
||||
R2router: 23712 Bytes
|
||||
Rivet: 24608 Bytes
|
||||
Tango: 28264 Bytes
|
||||
TigerTonic: 78768 Bytes
|
||||
Traffic: 538976 Bytes
|
||||
Vulcan: 369960 Bytes
|
||||
```
|
||||
- [Summary](#summary)
|
||||
- [Memory Consumption](#memory-consumption)
|
||||
- [Benchmark Results](#benchmark-results)
|
||||
- [GitHub API (203 routes)](#github-api-203-routes)
|
||||
- [Google+ API (13 routes)](#google-api-13-routes)
|
||||
- [Parse API (26 routes)](#parse-api-26-routes)
|
||||
- [Static Routes (157 routes)](#static-routes-157-routes)
|
||||
- [Micro Benchmarks](#micro-benchmarks)
|
||||
- [Single Param](#single-param)
|
||||
- [5 Params](#5-params)
|
||||
- [20 Params](#20-params)
|
||||
- [Param Write](#param-write)
|
||||
|
||||
## GithubAPI Routes: 203
|
||||
---
|
||||
|
||||
```sh
|
||||
Gin: 58512 Bytes
|
||||
## Summary
|
||||
|
||||
Ace: 48688 Bytes
|
||||
Aero: 318568 Bytes
|
||||
Bear: 84248 Bytes
|
||||
Beego: 150936 Bytes
|
||||
Bone: 100976 Bytes
|
||||
Chi: 95112 Bytes
|
||||
Denco: 36736 Bytes
|
||||
Echo: 100296 Bytes
|
||||
GocraftWeb: 95432 Bytes
|
||||
Goji: 49680 Bytes
|
||||
Gojiv2: 104704 Bytes
|
||||
GoJsonRest: 141976 Bytes
|
||||
GoRestful: 1241656 Bytes
|
||||
GorillaMux: 1322784 Bytes
|
||||
GowwwRouter: 80008 Bytes
|
||||
HttpRouter: 37144 Bytes
|
||||
HttpTreeMux: 78800 Bytes
|
||||
Kocha: 785120 Bytes
|
||||
LARS: 48600 Bytes
|
||||
Macaron: 92784 Bytes
|
||||
Martini: 485264 Bytes
|
||||
Pat: 21200 Bytes
|
||||
Possum: 85312 Bytes
|
||||
R2router: 47104 Bytes
|
||||
Rivet: 42840 Bytes
|
||||
Tango: 54840 Bytes
|
||||
TigerTonic: 95264 Bytes
|
||||
Traffic: 921744 Bytes
|
||||
Vulcan: 425992 Bytes
|
||||
```
|
||||
The table below ranks all routers by **GitHub API throughput** (203 routes, all methods), which best represents real-world routing workloads. _Lower ns/op is better._
|
||||
|
||||
## GPlusAPI Routes: 13
|
||||
| Rank | Router | ns/op | B/op | allocs/op | Zero-alloc |
|
||||
| :--: | :------------ | --------: | --------: | --------: | :----------------: |
|
||||
| 1 | **Gin** | 9,944 | 0 | 0 | :white_check_mark: |
|
||||
| 2 | **BunRouter** | 10,281 | 0 | 0 | :white_check_mark: |
|
||||
| 3 | **Echo** | 11,072 | 0 | 0 | :white_check_mark: |
|
||||
| 4 | HttpRouter | 15,059 | 13,792 | 167 | |
|
||||
| 5 | HttpTreeMux | 49,302 | 65,856 | 671 | |
|
||||
| 6 | Chi | 94,376 | 130,817 | 740 | |
|
||||
| 7 | Beego | 101,941 | 71,456 | 609 | |
|
||||
| 8 | Fiber | 109,148 | 0 | 0 | :white_check_mark: |
|
||||
| 9 | Macaron | 121,785 | 147,784 | 1,624 | |
|
||||
| 10 | Goji v2 | 242,849 | 313,744 | 3,712 | |
|
||||
| 11 | GoRestful | 885,678 | 1,006,744 | 3,009 | |
|
||||
| 12 | GorillaMux | 1,316,844 | 225,667 | 1,588 | |
|
||||
|
||||
```sh
|
||||
Gin: 4384 Bytes
|
||||
**Key takeaways:**
|
||||
|
||||
Ace: 3712 Bytes
|
||||
Aero: 26056 Bytes
|
||||
Bear: 7112 Bytes
|
||||
Beego: 10272 Bytes
|
||||
Bone: 6688 Bytes
|
||||
Chi: 8024 Bytes
|
||||
Denco: 3264 Bytes
|
||||
Echo: 9688 Bytes
|
||||
GocraftWeb: 7496 Bytes
|
||||
Goji: 3152 Bytes
|
||||
Gojiv2: 7376 Bytes
|
||||
GoJsonRest: 11400 Bytes
|
||||
GoRestful: 74328 Bytes
|
||||
GorillaMux: 66208 Bytes
|
||||
GowwwRouter: 5744 Bytes
|
||||
HttpRouter: 2808 Bytes
|
||||
HttpTreeMux: 7440 Bytes
|
||||
Kocha: 128880 Bytes
|
||||
LARS: 3656 Bytes
|
||||
Macaron: 8656 Bytes
|
||||
Martini: 23920 Bytes
|
||||
Pat: 1856 Bytes
|
||||
Possum: 7248 Bytes
|
||||
R2router: 3928 Bytes
|
||||
Rivet: 3064 Bytes
|
||||
Tango: 5168 Bytes
|
||||
TigerTonic: 9408 Bytes
|
||||
Traffic: 46400 Bytes
|
||||
Vulcan: 25544 Bytes
|
||||
```
|
||||
- **Gin**, **BunRouter**, and **Echo** form the top tier — all achieve zero heap allocations and route the full GitHub API in ~10 us.
|
||||
- **HttpRouter** remains extremely fast but incurs 1 alloc per parameterized route (167 allocs for 203 routes).
|
||||
- **Fiber** also achieves zero allocations, but its fasthttp-based benchmark infrastructure adds per-iteration reset overhead — do not compare its absolute ns/op directly with net/http routers.
|
||||
- **GorillaMux** and **GoRestful** are feature-rich but orders of magnitude slower, making them less suitable for latency-sensitive applications.
|
||||
|
||||
## ParseAPI Routes: 26
|
||||
> **Fiber caveat:** Fiber benchmarks use `fasthttp.RequestCtx` with per-iteration Reset, which adds constant overhead not present in net/http benchmarks. Fiber-vs-Fiber comparisons are valid; cross-framework comparisons should be interpreted with care.
|
||||
|
||||
```sh
|
||||
Gin: 7776 Bytes
|
||||
---
|
||||
|
||||
Ace: 6704 Bytes
|
||||
Aero: 28488 Bytes
|
||||
Bear: 12320 Bytes
|
||||
Beego: 19280 Bytes
|
||||
Bone: 11440 Bytes
|
||||
Chi: 9744 Bytes
|
||||
Denco: 4192 Bytes
|
||||
Echo: 11664 Bytes
|
||||
GocraftWeb: 12800 Bytes
|
||||
Goji: 5680 Bytes
|
||||
Gojiv2: 14464 Bytes
|
||||
GoJsonRest: 14072 Bytes
|
||||
GoRestful: 116264 Bytes
|
||||
GorillaMux: 105880 Bytes
|
||||
GowwwRouter: 9344 Bytes
|
||||
HttpRouter: 5072 Bytes
|
||||
HttpTreeMux: 7848 Bytes
|
||||
Kocha: 181712 Bytes
|
||||
LARS: 6632 Bytes
|
||||
Macaron: 13648 Bytes
|
||||
Martini: 45888 Bytes
|
||||
Pat: 2560 Bytes
|
||||
Possum: 9200 Bytes
|
||||
R2router: 7056 Bytes
|
||||
Rivet: 5680 Bytes
|
||||
Tango: 8920 Bytes
|
||||
TigerTonic: 9840 Bytes
|
||||
Traffic: 79096 Bytes
|
||||
Vulcan: 44504 Bytes
|
||||
```
|
||||
## Memory Consumption
|
||||
|
||||
## Static Routes
|
||||
Memory required for loading the routing structure (lower is better). Sorted by bytes ascending.
|
||||
|
||||
```sh
|
||||
BenchmarkGin_StaticAll 62169 19319 ns/op 0 B/op 0 allocs/op
|
||||
### Static Routes: 157
|
||||
|
||||
BenchmarkAce_StaticAll 65428 18313 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_StaticAll 121132 9632 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpServeMux_StaticAll 52626 22758 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBeego_StaticAll 9962 179058 ns/op 55264 B/op 471 allocs/op
|
||||
BenchmarkBear_StaticAll 14894 80966 ns/op 20272 B/op 469 allocs/op
|
||||
BenchmarkBone_StaticAll 18718 64065 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChi_StaticAll 10000 149827 ns/op 67824 B/op 471 allocs/op
|
||||
BenchmarkDenco_StaticAll 211393 5680 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkEcho_StaticAll 49341 24343 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_StaticAll 10000 126209 ns/op 46312 B/op 785 allocs/op
|
||||
BenchmarkGoji_StaticAll 27956 43174 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGojiv2_StaticAll 3430 370718 ns/op 205984 B/op 1570 allocs/op
|
||||
BenchmarkGoJsonRest_StaticAll 9134 188888 ns/op 51653 B/op 1727 allocs/op
|
||||
BenchmarkGoRestful_StaticAll 706 1703330 ns/op 613280 B/op 2053 allocs/op
|
||||
BenchmarkGorillaMux_StaticAll 1268 924083 ns/op 153233 B/op 1413 allocs/op
|
||||
BenchmarkGowwwRouter_StaticAll 63374 18935 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpRouter_StaticAll 109938 10902 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_StaticAll 109166 10861 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkKocha_StaticAll 92258 12992 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLARS_StaticAll 65200 18387 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_StaticAll 5671 291501 ns/op 115553 B/op 1256 allocs/op
|
||||
BenchmarkMartini_StaticAll 807 1460498 ns/op 125444 B/op 1717 allocs/op
|
||||
BenchmarkPat_StaticAll 513 2342396 ns/op 602832 B/op 12559 allocs/op
|
||||
BenchmarkPossum_StaticAll 10000 128270 ns/op 65312 B/op 471 allocs/op
|
||||
BenchmarkR2router_StaticAll 16726 71760 ns/op 22608 B/op 628 allocs/op
|
||||
BenchmarkRivet_StaticAll 41722 28723 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkTango_StaticAll 7606 205082 ns/op 39209 B/op 1256 allocs/op
|
||||
BenchmarkTigerTonic_StaticAll 26247 45806 ns/op 7376 B/op 157 allocs/op
|
||||
BenchmarkTraffic_StaticAll 550 2284518 ns/op 754864 B/op 14601 allocs/op
|
||||
BenchmarkVulcan_StaticAll 10000 131343 ns/op 15386 B/op 471 allocs/op
|
||||
```
|
||||
| Router | Bytes |
|
||||
| :------------- | ---------: |
|
||||
| **HttpRouter** | **21,680** |
|
||||
| **Gin** | **34,408** |
|
||||
| **Macaron** | **36,976** |
|
||||
| BunRouter | 51,232 |
|
||||
| Fiber | 59,248 |
|
||||
| HttpServeMux | 71,728 |
|
||||
| HttpTreeMux | 73,448 |
|
||||
| Chi | 83,160 |
|
||||
| Echo | 91,976 |
|
||||
| Beego | 98,824 |
|
||||
| Goji v2 | 117,952 |
|
||||
| GorillaMux | 599,496 |
|
||||
| GoRestful | 819,704 |
|
||||
|
||||
### GitHub API Routes: 203
|
||||
|
||||
| Router | Bytes |
|
||||
| :-------------- | ---------: |
|
||||
| **HttpRouter** | **37,072** |
|
||||
| **Gin** | **58,840** |
|
||||
| **HttpTreeMux** | **78,800** |
|
||||
| Macaron | 90,632 |
|
||||
| BunRouter | 93,776 |
|
||||
| Chi | 94,888 |
|
||||
| Echo | 117,784 |
|
||||
| Goji v2 | 118,640 |
|
||||
| Beego | 150,840 |
|
||||
| Fiber | 163,832 |
|
||||
| GoRestful | 1,270,848 |
|
||||
| GorillaMux | 1,319,696 |
|
||||
|
||||
### Google+ API Routes: 13
|
||||
|
||||
| Router | Bytes |
|
||||
| :------------- | --------: |
|
||||
| **HttpRouter** | **2,776** |
|
||||
| **Gin** | **4,576** |
|
||||
| **BunRouter** | **7,360** |
|
||||
| HttpTreeMux | 7,440 |
|
||||
| Chi | 8,008 |
|
||||
| Goji v2 | 8,096 |
|
||||
| Macaron | 8,672 |
|
||||
| Beego | 10,256 |
|
||||
| Fiber | 10,840 |
|
||||
| Echo | 10,968 |
|
||||
| GorillaMux | 68,000 |
|
||||
| GoRestful | 72,536 |
|
||||
|
||||
### Parse API Routes: 26
|
||||
|
||||
| Router | Bytes |
|
||||
| :-------------- | --------: |
|
||||
| **HttpRouter** | **5,024** |
|
||||
| **Gin** | **7,896** |
|
||||
| **HttpTreeMux** | **7,848** |
|
||||
| BunRouter | 9,336 |
|
||||
| Chi | 9,656 |
|
||||
| Echo | 13,816 |
|
||||
| Macaron | 13,704 |
|
||||
| Fiber | 15,352 |
|
||||
| Goji v2 | 16,064 |
|
||||
| Beego | 19,256 |
|
||||
| GorillaMux | 105,384 |
|
||||
| GoRestful | 121,200 |
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Results
|
||||
|
||||
### GitHub API (203 routes)
|
||||
|
||||
Routing all 203 GitHub API endpoints per operation.
|
||||
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | --------: | --------: | --------: |
|
||||
| 1 | **Gin** | 9,944 | 0 | 0 |
|
||||
| 2 | **BunRouter** | 10,281 | 0 | 0 |
|
||||
| 3 | **Echo** | 11,072 | 0 | 0 |
|
||||
| 4 | HttpRouter | 15,059 | 13,792 | 167 |
|
||||
| 5 | HttpTreeMux | 49,302 | 65,856 | 671 |
|
||||
| 6 | Chi | 94,376 | 130,817 | 740 |
|
||||
| 7 | Beego | 101,941 | 71,456 | 609 |
|
||||
| 8 | Fiber | 109,148 | 0 | 0 |
|
||||
| 9 | Macaron | 121,785 | 147,784 | 1,624 |
|
||||
| 10 | Goji v2 | 242,849 | 313,744 | 3,712 |
|
||||
| 11 | GoRestful | 885,678 | 1,006,744 | 3,009 |
|
||||
| 12 | GorillaMux | 1,316,844 | 225,667 | 1,588 |
|
||||
|
||||
### Google+ API (13 routes)
|
||||
|
||||
Routing all 13 Google+ API endpoints per operation.
|
||||
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | -----: | -----: | --------: |
|
||||
| 1 | **BunRouter** | 348.5 | 0 | 0 |
|
||||
| 2 | **Gin** | 429.7 | 0 | 0 |
|
||||
| 3 | **Echo** | 451.1 | 0 | 0 |
|
||||
| 4 | HttpRouter | 668.6 | 640 | 11 |
|
||||
| 5 | HttpTreeMux | 2,428 | 4,032 | 38 |
|
||||
| 6 | Fiber | 2,506 | 0 | 0 |
|
||||
| 7 | Chi | 5,333 | 8,480 | 48 |
|
||||
| 8 | Beego | 5,927 | 4,576 | 39 |
|
||||
| 9 | Macaron | 7,294 | 9,464 | 104 |
|
||||
| 10 | Goji v2 | 8,000 | 15,120 | 115 |
|
||||
| 11 | GorillaMux | 14,707 | 14,448 | 102 |
|
||||
| 12 | GoRestful | 24,189 | 60,720 | 193 |
|
||||
|
||||
### Parse API (26 routes)
|
||||
|
||||
Routing all 26 Parse API endpoints per operation.
|
||||
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | -----: | ------: | --------: |
|
||||
| 1 | **BunRouter** | 588.2 | 0 | 0 |
|
||||
| 2 | **Gin** | 712.1 | 0 | 0 |
|
||||
| 3 | **Echo** | 742.1 | 0 | 0 |
|
||||
| 4 | HttpRouter | 948.5 | 640 | 16 |
|
||||
| 5 | HttpTreeMux | 3,372 | 5,728 | 51 |
|
||||
| 6 | Fiber | 4,250 | 0 | 0 |
|
||||
| 7 | Chi | 8,863 | 14,944 | 84 |
|
||||
| 8 | Beego | 10,541 | 9,152 | 78 |
|
||||
| 9 | Macaron | 13,635 | 18,928 | 208 |
|
||||
| 10 | Goji v2 | 13,264 | 29,456 | 199 |
|
||||
| 11 | GorillaMux | 25,886 | 26,960 | 198 |
|
||||
| 12 | GoRestful | 54,780 | 131,728 | 380 |
|
||||
|
||||
### Static Routes (157 routes)
|
||||
|
||||
Routing all 157 static routes per operation. Includes http.ServeMux as baseline.
|
||||
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :-------------- | ------: | ------: | --------: |
|
||||
| 1 | **HttpRouter** | 4,177 | 0 | 0 |
|
||||
| 2 | **HttpTreeMux** | 5,363 | 0 | 0 |
|
||||
| 3 | **Gin** | 5,528 | 0 | 0 |
|
||||
| 4 | BunRouter | 5,997 | 0 | 0 |
|
||||
| 5 | Echo | 6,897 | 0 | 0 |
|
||||
| — | HttpServeMux | 18,172 | 0 | 0 |
|
||||
| 6 | Fiber | 29,310 | 0 | 0 |
|
||||
| 7 | Chi | 41,317 | 57,776 | 314 |
|
||||
| 8 | Beego | 68,255 | 55,264 | 471 |
|
||||
| 9 | Macaron | 81,824 | 114,296 | 1,256 |
|
||||
| 10 | Goji v2 | 84,459 | 175,840 | 1,099 |
|
||||
| 11 | GorillaMux | 302,825 | 133,137 | 1,099 |
|
||||
| 12 | GoRestful | 436,510 | 677,824 | 2,193 |
|
||||
|
||||
---
|
||||
|
||||
## Micro Benchmarks
|
||||
|
||||
```sh
|
||||
BenchmarkGin_Param 18785022 63.9 ns/op 0 B/op 0 allocs/op
|
||||
### Single Param
|
||||
|
||||
BenchmarkAce_Param 14689765 81.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_Param 23094770 51.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_Param 1417045 845 ns/op 456 B/op 5 allocs/op
|
||||
BenchmarkBeego_Param 1000000 1080 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_Param 1000000 1463 ns/op 816 B/op 6 allocs/op
|
||||
BenchmarkChi_Param 1378756 885 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_Param 8557899 143 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkEcho_Param 16433347 75.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_Param 1000000 1218 ns/op 648 B/op 8 allocs/op
|
||||
BenchmarkGoji_Param 1921248 617 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_Param 561848 2156 ns/op 1328 B/op 11 allocs/op
|
||||
BenchmarkGoJsonRest_Param 1000000 1358 ns/op 649 B/op 13 allocs/op
|
||||
BenchmarkGoRestful_Param 224857 5307 ns/op 4192 B/op 14 allocs/op
|
||||
BenchmarkGorillaMux_Param 498313 2459 ns/op 1280 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_Param 1864354 654 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_Param 26269074 47.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_Param 2109829 557 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkKocha_Param 5050216 243 ns/op 56 B/op 3 allocs/op
|
||||
BenchmarkLARS_Param 19811712 59.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_Param 662746 2329 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_Param 279902 4260 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkPat_Param 1000000 1382 ns/op 536 B/op 11 allocs/op
|
||||
BenchmarkPossum_Param 1000000 1014 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_Param 1712559 707 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_Param 6648086 182 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkTango_Param 1221504 994 ns/op 248 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_Param 891661 2261 ns/op 776 B/op 16 allocs/op
|
||||
BenchmarkTraffic_Param 350059 3598 ns/op 1856 B/op 21 allocs/op
|
||||
BenchmarkVulcan_Param 2517823 472 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_Param5 9214365 130 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_Param5 15369013 77.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_Param5 1000000 1113 ns/op 501 B/op 5 allocs/op
|
||||
BenchmarkBeego_Param5 1000000 1269 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_Param5 986820 1873 ns/op 864 B/op 6 allocs/op
|
||||
BenchmarkChi_Param5 1000000 1156 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_Param5 3036331 400 ns/op 160 B/op 1 allocs/op
|
||||
BenchmarkEcho_Param5 6447133 186 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_Param5 10786068 110 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_Param5 844820 1944 ns/op 920 B/op 11 allocs/op
|
||||
BenchmarkGoji_Param5 1474965 827 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_Param5 442820 2516 ns/op 1392 B/op 11 allocs/op
|
||||
BenchmarkGoJsonRest_Param5 507555 2711 ns/op 1097 B/op 16 allocs/op
|
||||
BenchmarkGoRestful_Param5 216481 6093 ns/op 4288 B/op 14 allocs/op
|
||||
BenchmarkGorillaMux_Param5 314402 3628 ns/op 1344 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_Param5 1624660 733 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_Param5 13167324 92.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_Param5 1000000 1295 ns/op 576 B/op 6 allocs/op
|
||||
BenchmarkKocha_Param5 1000000 1138 ns/op 440 B/op 10 allocs/op
|
||||
BenchmarkLARS_Param5 11580613 105 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_Param5 473596 2755 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_Param5 230756 5111 ns/op 1232 B/op 11 allocs/op
|
||||
BenchmarkPat_Param5 469190 3370 ns/op 888 B/op 29 allocs/op
|
||||
BenchmarkPossum_Param5 1000000 1002 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_Param5 1422129 844 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_Param5 2263789 539 ns/op 240 B/op 1 allocs/op
|
||||
BenchmarkTango_Param5 1000000 1256 ns/op 360 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_Param5 175500 7492 ns/op 2279 B/op 39 allocs/op
|
||||
BenchmarkTraffic_Param5 233631 5816 ns/op 2208 B/op 27 allocs/op
|
||||
BenchmarkVulcan_Param5 1923416 629 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_Param20 4321266 281 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_Param20 31501641 35.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_Param20 335204 3489 ns/op 1665 B/op 5 allocs/op
|
||||
BenchmarkBeego_Param20 503674 2860 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_Param20 298922 4741 ns/op 2031 B/op 6 allocs/op
|
||||
BenchmarkChi_Param20 878181 1957 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_Param20 1000000 1360 ns/op 640 B/op 1 allocs/op
|
||||
BenchmarkEcho_Param20 2104946 580 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_Param20 4167204 290 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_Param20 173064 7514 ns/op 3796 B/op 15 allocs/op
|
||||
BenchmarkGoji_Param20 458778 2651 ns/op 1247 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_Param20 364862 3178 ns/op 1632 B/op 11 allocs/op
|
||||
BenchmarkGoJsonRest_Param20 125514 9760 ns/op 4485 B/op 20 allocs/op
|
||||
BenchmarkGoRestful_Param20 101217 11964 ns/op 6715 B/op 18 allocs/op
|
||||
BenchmarkGorillaMux_Param20 147654 8132 ns/op 3452 B/op 12 allocs/op
|
||||
BenchmarkGowwwRouter_Param20 1000000 1225 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_Param20 4920895 247 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_Param20 173202 6605 ns/op 3196 B/op 10 allocs/op
|
||||
BenchmarkKocha_Param20 345988 3620 ns/op 1808 B/op 27 allocs/op
|
||||
BenchmarkLARS_Param20 4592326 262 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_Param20 166492 7286 ns/op 2924 B/op 12 allocs/op
|
||||
BenchmarkMartini_Param20 122162 10653 ns/op 3595 B/op 13 allocs/op
|
||||
BenchmarkPat_Param20 78630 15239 ns/op 4424 B/op 93 allocs/op
|
||||
BenchmarkPossum_Param20 1000000 1008 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_Param20 294981 4587 ns/op 2284 B/op 7 allocs/op
|
||||
BenchmarkRivet_Param20 691798 2090 ns/op 1024 B/op 1 allocs/op
|
||||
BenchmarkTango_Param20 842440 2505 ns/op 856 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_Param20 38614 31509 ns/op 9870 B/op 119 allocs/op
|
||||
BenchmarkTraffic_Param20 57633 21107 ns/op 7853 B/op 47 allocs/op
|
||||
BenchmarkVulcan_Param20 1000000 1178 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_ParamWrite 7330743 180 ns/op 8 B/op 1 allocs/op
|
||||
BenchmarkAero_ParamWrite 13833598 86.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_ParamWrite 1363321 867 ns/op 456 B/op 5 allocs/op
|
||||
BenchmarkBeego_ParamWrite 1000000 1104 ns/op 360 B/op 4 allocs/op
|
||||
BenchmarkBone_ParamWrite 1000000 1475 ns/op 816 B/op 6 allocs/op
|
||||
BenchmarkChi_ParamWrite 1320590 892 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_ParamWrite 7093605 172 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkEcho_ParamWrite 8434424 161 ns/op 8 B/op 1 allocs/op
|
||||
BenchmarkGin_ParamWrite 10377034 118 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_ParamWrite 1000000 1266 ns/op 656 B/op 9 allocs/op
|
||||
BenchmarkGoji_ParamWrite 1874168 654 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_ParamWrite 459032 2352 ns/op 1360 B/op 13 allocs/op
|
||||
BenchmarkGoJsonRest_ParamWrite 499434 2145 ns/op 1128 B/op 18 allocs/op
|
||||
BenchmarkGoRestful_ParamWrite 241087 5470 ns/op 4200 B/op 15 allocs/op
|
||||
BenchmarkGorillaMux_ParamWrite 425686 2522 ns/op 1280 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_ParamWrite 922172 1778 ns/op 976 B/op 8 allocs/op
|
||||
BenchmarkHttpRouter_ParamWrite 15392049 77.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_ParamWrite 1973385 597 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkKocha_ParamWrite 4262500 281 ns/op 56 B/op 3 allocs/op
|
||||
BenchmarkLARS_ParamWrite 10764410 113 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_ParamWrite 486769 2726 ns/op 1176 B/op 14 allocs/op
|
||||
BenchmarkMartini_ParamWrite 264804 4842 ns/op 1176 B/op 14 allocs/op
|
||||
BenchmarkPat_ParamWrite 735116 2047 ns/op 960 B/op 15 allocs/op
|
||||
BenchmarkPossum_ParamWrite 1000000 1004 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_ParamWrite 1592136 768 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_ParamWrite 3582051 339 ns/op 112 B/op 2 allocs/op
|
||||
BenchmarkTango_ParamWrite 2237337 534 ns/op 136 B/op 4 allocs/op
|
||||
BenchmarkTigerTonic_ParamWrite 439608 3136 ns/op 1216 B/op 21 allocs/op
|
||||
BenchmarkTraffic_ParamWrite 306979 4328 ns/op 2280 B/op 25 allocs/op
|
||||
BenchmarkVulcan_ParamWrite 2529973 472 ns/op 98 B/op 3 allocs/op
|
||||
```
|
||||
Route: `/user/:name` — Request: `GET /user/gordon`
|
||||
|
||||
## GitHub
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | ----: | ----: | --------: |
|
||||
| 1 | **BunRouter** | 12.22 | 0 | 0 |
|
||||
| 2 | **Echo** | 17.75 | 0 | 0 |
|
||||
| 3 | **Gin** | 23.31 | 0 | 0 |
|
||||
| 4 | HttpRouter | 31.88 | 32 | 1 |
|
||||
| 5 | Fiber | 114.4 | 0 | 0 |
|
||||
| 6 | HttpTreeMux | 165.0 | 352 | 3 |
|
||||
| 7 | Chi | 332.2 | 704 | 4 |
|
||||
| 8 | Beego | 348.8 | 352 | 3 |
|
||||
| 9 | Goji v2 | 494.3 | 1,136 | 8 |
|
||||
| 10 | GorillaMux | 630.6 | 1,152 | 8 |
|
||||
| 11 | Macaron | 708.0 | 1,064 | 10 |
|
||||
| 12 | GoRestful | 1,394 | 4,600 | 15 |
|
||||
|
||||
```sh
|
||||
BenchmarkGin_GithubStatic 15629472 76.7 ns/op 0 B/op 0 allocs/op
|
||||
### 5 Params
|
||||
|
||||
BenchmarkAce_GithubStatic 15542612 75.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GithubStatic 24777151 48.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GithubStatic 2788894 435 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkBeego_GithubStatic 1000000 1064 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_GithubStatic 93507 12838 ns/op 2880 B/op 60 allocs/op
|
||||
BenchmarkChi_GithubStatic 1387743 860 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_GithubStatic 39384996 30.4 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkEcho_GithubStatic 12076382 99.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GithubStatic 1596495 756 ns/op 296 B/op 5 allocs/op
|
||||
BenchmarkGoji_GithubStatic 6364876 189 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGojiv2_GithubStatic 550202 2098 ns/op 1312 B/op 10 allocs/op
|
||||
BenchmarkGoRestful_GithubStatic 102183 12552 ns/op 4256 B/op 13 allocs/op
|
||||
BenchmarkGoJsonRest_GithubStatic 1000000 1029 ns/op 329 B/op 11 allocs/op
|
||||
BenchmarkGorillaMux_GithubStatic 255552 5190 ns/op 976 B/op 9 allocs/op
|
||||
BenchmarkGowwwRouter_GithubStatic 15531916 77.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpRouter_GithubStatic 27920724 43.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GithubStatic 21448953 55.8 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkKocha_GithubStatic 21405310 56.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLARS_GithubStatic 13625156 89.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GithubStatic 1000000 1747 ns/op 736 B/op 8 allocs/op
|
||||
BenchmarkMartini_GithubStatic 187186 7326 ns/op 768 B/op 9 allocs/op
|
||||
BenchmarkPat_GithubStatic 109143 11563 ns/op 3648 B/op 76 allocs/op
|
||||
BenchmarkPossum_GithubStatic 1575898 770 ns/op 416 B/op 3 allocs/op
|
||||
BenchmarkR2router_GithubStatic 3046231 404 ns/op 144 B/op 4 allocs/op
|
||||
BenchmarkRivet_GithubStatic 11484826 105 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkTango_GithubStatic 1000000 1153 ns/op 248 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_GithubStatic 4929780 249 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkTraffic_GithubStatic 106351 11819 ns/op 4664 B/op 90 allocs/op
|
||||
BenchmarkVulcan_GithubStatic 1613271 722 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_GithubParam 8386032 143 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GithubParam 11816200 102 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GithubParam 1000000 1012 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkBeego_GithubParam 1000000 1157 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_GithubParam 184653 6912 ns/op 1888 B/op 19 allocs/op
|
||||
BenchmarkChi_GithubParam 1000000 1102 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_GithubParam 3484798 352 ns/op 128 B/op 1 allocs/op
|
||||
BenchmarkEcho_GithubParam 6337380 189 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_GithubParam 9132032 131 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GithubParam 1000000 1446 ns/op 712 B/op 9 allocs/op
|
||||
BenchmarkGoji_GithubParam 1248640 977 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_GithubParam 383233 2784 ns/op 1408 B/op 13 allocs/op
|
||||
BenchmarkGoJsonRest_GithubParam 1000000 1991 ns/op 713 B/op 14 allocs/op
|
||||
BenchmarkGoRestful_GithubParam 76414 16015 ns/op 4352 B/op 16 allocs/op
|
||||
BenchmarkGorillaMux_GithubParam 150026 7663 ns/op 1296 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_GithubParam 1592044 751 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_GithubParam 10420628 115 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GithubParam 1403755 835 ns/op 384 B/op 4 allocs/op
|
||||
BenchmarkKocha_GithubParam 2286170 533 ns/op 128 B/op 5 allocs/op
|
||||
BenchmarkLARS_GithubParam 9540374 129 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GithubParam 533154 2742 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_GithubParam 119397 9638 ns/op 1152 B/op 11 allocs/op
|
||||
BenchmarkPat_GithubParam 150675 8858 ns/op 2408 B/op 48 allocs/op
|
||||
BenchmarkPossum_GithubParam 1000000 1001 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_GithubParam 1602886 761 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_GithubParam 2986579 409 ns/op 96 B/op 1 allocs/op
|
||||
BenchmarkTango_GithubParam 1000000 1356 ns/op 344 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_GithubParam 388899 3429 ns/op 1176 B/op 22 allocs/op
|
||||
BenchmarkTraffic_GithubParam 123160 9734 ns/op 2816 B/op 40 allocs/op
|
||||
BenchmarkVulcan_GithubParam 1000000 1138 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_GithubAll 40543 29670 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GithubAll 57632 20648 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GithubAll 9234 216179 ns/op 86448 B/op 943 allocs/op
|
||||
BenchmarkBeego_GithubAll 7407 243496 ns/op 71456 B/op 609 allocs/op
|
||||
BenchmarkBone_GithubAll 420 2922835 ns/op 720160 B/op 8620 allocs/op
|
||||
BenchmarkChi_GithubAll 7620 238331 ns/op 87696 B/op 609 allocs/op
|
||||
BenchmarkDenco_GithubAll 18355 64494 ns/op 20224 B/op 167 allocs/op
|
||||
BenchmarkEcho_GithubAll 31251 38479 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_GithubAll 43550 27364 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GithubAll 4117 300062 ns/op 131656 B/op 1686 allocs/op
|
||||
BenchmarkGoji_GithubAll 3274 416158 ns/op 56112 B/op 334 allocs/op
|
||||
BenchmarkGojiv2_GithubAll 1402 870518 ns/op 352720 B/op 4321 allocs/op
|
||||
BenchmarkGoJsonRest_GithubAll 2976 401507 ns/op 134371 B/op 2737 allocs/op
|
||||
BenchmarkGoRestful_GithubAll 410 2913158 ns/op 910144 B/op 2938 allocs/op
|
||||
BenchmarkGorillaMux_GithubAll 346 3384987 ns/op 251650 B/op 1994 allocs/op
|
||||
BenchmarkGowwwRouter_GithubAll 10000 143025 ns/op 72144 B/op 501 allocs/op
|
||||
BenchmarkHttpRouter_GithubAll 55938 21360 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GithubAll 10000 153944 ns/op 65856 B/op 671 allocs/op
|
||||
BenchmarkKocha_GithubAll 10000 106315 ns/op 23304 B/op 843 allocs/op
|
||||
BenchmarkLARS_GithubAll 47779 25084 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GithubAll 3266 371907 ns/op 149409 B/op 1624 allocs/op
|
||||
BenchmarkMartini_GithubAll 331 3444706 ns/op 226551 B/op 2325 allocs/op
|
||||
BenchmarkPat_GithubAll 273 4381818 ns/op 1483152 B/op 26963 allocs/op
|
||||
BenchmarkPossum_GithubAll 10000 164367 ns/op 84448 B/op 609 allocs/op
|
||||
BenchmarkR2router_GithubAll 10000 160220 ns/op 77328 B/op 979 allocs/op
|
||||
BenchmarkRivet_GithubAll 14625 82453 ns/op 16272 B/op 167 allocs/op
|
||||
BenchmarkTango_GithubAll 6255 279611 ns/op 63826 B/op 1618 allocs/op
|
||||
BenchmarkTigerTonic_GithubAll 2008 687874 ns/op 193856 B/op 4474 allocs/op
|
||||
BenchmarkTraffic_GithubAll 355 3478508 ns/op 820744 B/op 14114 allocs/op
|
||||
BenchmarkVulcan_GithubAll 6885 193333 ns/op 19894 B/op 609 allocs/op
|
||||
```
|
||||
Route: `/:a/:b/:c/:d/:e` — Request: `GET /test/test/test/test/test`
|
||||
|
||||
## Google+
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | ----: | ----: | --------: |
|
||||
| 1 | **BunRouter** | 41.86 | 0 | 0 |
|
||||
| 2 | **Echo** | 43.76 | 0 | 0 |
|
||||
| 3 | **Gin** | 44.20 | 0 | 0 |
|
||||
| 4 | HttpRouter | 83.74 | 160 | 1 |
|
||||
| 5 | Fiber | 271.6 | 0 | 0 |
|
||||
| 6 | HttpTreeMux | 358.8 | 576 | 6 |
|
||||
| 7 | Chi | 453.7 | 704 | 4 |
|
||||
| 8 | Beego | 480.3 | 352 | 3 |
|
||||
| 9 | Goji v2 | 532.4 | 1,200 | 8 |
|
||||
| 10 | Macaron | 799.7 | 1,064 | 10 |
|
||||
| 11 | GorillaMux | 972.6 | 1,216 | 8 |
|
||||
| 12 | GoRestful | 1,579 | 4,712 | 15 |
|
||||
|
||||
```sh
|
||||
BenchmarkGin_GPlusStatic 19247326 62.2 ns/op 0 B/op 0 allocs/op
|
||||
### 20 Params
|
||||
|
||||
BenchmarkAce_GPlusStatic 20235060 59.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GPlusStatic 31978935 37.6 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GPlusStatic 3516523 341 ns/op 104 B/op 3 allocs/op
|
||||
BenchmarkBeego_GPlusStatic 1212036 991 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_GPlusStatic 6736242 183 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkChi_GPlusStatic 1490640 814 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_GPlusStatic 55006856 21.8 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkEcho_GPlusStatic 17688258 67.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GPlusStatic 1829181 666 ns/op 280 B/op 5 allocs/op
|
||||
BenchmarkGoji_GPlusStatic 9147451 130 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGojiv2_GPlusStatic 594015 2063 ns/op 1312 B/op 10 allocs/op
|
||||
BenchmarkGoJsonRest_GPlusStatic 1264906 950 ns/op 329 B/op 11 allocs/op
|
||||
BenchmarkGoRestful_GPlusStatic 231558 5341 ns/op 3872 B/op 13 allocs/op
|
||||
BenchmarkGorillaMux_GPlusStatic 908418 1809 ns/op 976 B/op 9 allocs/op
|
||||
BenchmarkGowwwRouter_GPlusStatic 40684604 29.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpRouter_GPlusStatic 46742804 25.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GPlusStatic 32567161 36.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkKocha_GPlusStatic 33800060 35.3 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLARS_GPlusStatic 20431858 60.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GPlusStatic 1000000 1745 ns/op 736 B/op 8 allocs/op
|
||||
BenchmarkMartini_GPlusStatic 442248 3619 ns/op 768 B/op 9 allocs/op
|
||||
BenchmarkPat_GPlusStatic 4328004 292 ns/op 96 B/op 2 allocs/op
|
||||
BenchmarkPossum_GPlusStatic 1570753 763 ns/op 416 B/op 3 allocs/op
|
||||
BenchmarkR2router_GPlusStatic 3339474 355 ns/op 144 B/op 4 allocs/op
|
||||
BenchmarkRivet_GPlusStatic 18570961 64.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkTango_GPlusStatic 1388702 860 ns/op 200 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_GPlusStatic 7803543 159 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkTraffic_GPlusStatic 878605 2171 ns/op 1112 B/op 16 allocs/op
|
||||
BenchmarkVulcan_GPlusStatic 2742446 437 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_GPlusParam 11626975 105 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GPlusParam 16914322 71.6 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GPlusParam 1405173 832 ns/op 480 B/op 5 allocs/op
|
||||
BenchmarkBeego_GPlusParam 1000000 1075 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_GPlusParam 1000000 1557 ns/op 816 B/op 6 allocs/op
|
||||
BenchmarkChi_GPlusParam 1347926 894 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_GPlusParam 5513000 212 ns/op 64 B/op 1 allocs/op
|
||||
BenchmarkEcho_GPlusParam 11884383 101 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_GPlusParam 12898952 93.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GPlusParam 1000000 1194 ns/op 648 B/op 8 allocs/op
|
||||
BenchmarkGoji_GPlusParam 1857229 645 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_GPlusParam 520939 2322 ns/op 1328 B/op 11 allocs/op
|
||||
BenchmarkGoJsonRest_GPlusParam 1000000 1536 ns/op 649 B/op 13 allocs/op
|
||||
BenchmarkGoRestful_GPlusParam 205449 5800 ns/op 4192 B/op 14 allocs/op
|
||||
BenchmarkGorillaMux_GPlusParam 395310 3188 ns/op 1280 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_GPlusParam 1851798 667 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_GPlusParam 18420789 65.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GPlusParam 1878463 629 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkKocha_GPlusParam 4495610 273 ns/op 56 B/op 3 allocs/op
|
||||
BenchmarkLARS_GPlusParam 14615976 83.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GPlusParam 584145 2549 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_GPlusParam 250501 4583 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkPat_GPlusParam 1000000 1645 ns/op 576 B/op 11 allocs/op
|
||||
BenchmarkPossum_GPlusParam 1000000 1008 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_GPlusParam 1708191 688 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_GPlusParam 5795014 211 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkTango_GPlusParam 1000000 1091 ns/op 264 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_GPlusParam 760221 2489 ns/op 856 B/op 16 allocs/op
|
||||
BenchmarkTraffic_GPlusParam 309774 4039 ns/op 1872 B/op 21 allocs/op
|
||||
BenchmarkVulcan_GPlusParam 1935730 623 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_GPlus2Params 9158314 134 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GPlus2Params 11300517 107 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GPlus2Params 1239238 961 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkBeego_GPlus2Params 1000000 1202 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_GPlus2Params 335576 3725 ns/op 1168 B/op 10 allocs/op
|
||||
BenchmarkChi_GPlus2Params 1000000 1014 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_GPlus2Params 4394598 280 ns/op 64 B/op 1 allocs/op
|
||||
BenchmarkEcho_GPlus2Params 7851861 154 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_GPlus2Params 9958588 120 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GPlus2Params 1000000 1433 ns/op 712 B/op 9 allocs/op
|
||||
BenchmarkGoji_GPlus2Params 1325134 909 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_GPlus2Params 405955 2870 ns/op 1408 B/op 14 allocs/op
|
||||
BenchmarkGoJsonRest_GPlus2Params 977038 1987 ns/op 713 B/op 14 allocs/op
|
||||
BenchmarkGoRestful_GPlus2Params 205018 6142 ns/op 4384 B/op 16 allocs/op
|
||||
BenchmarkGorillaMux_GPlus2Params 205641 6015 ns/op 1296 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_GPlus2Params 1748542 684 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_GPlus2Params 14047102 87.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GPlus2Params 1418673 828 ns/op 384 B/op 4 allocs/op
|
||||
BenchmarkKocha_GPlus2Params 2334562 520 ns/op 128 B/op 5 allocs/op
|
||||
BenchmarkLARS_GPlus2Params 11954094 101 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GPlus2Params 491552 2890 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_GPlus2Params 120532 9545 ns/op 1200 B/op 13 allocs/op
|
||||
BenchmarkPat_GPlus2Params 194739 6766 ns/op 2168 B/op 33 allocs/op
|
||||
BenchmarkPossum_GPlus2Params 1201224 1009 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_GPlus2Params 1575535 756 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_GPlus2Params 3698930 325 ns/op 96 B/op 1 allocs/op
|
||||
BenchmarkTango_GPlus2Params 1000000 1212 ns/op 344 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_GPlus2Params 349350 3660 ns/op 1200 B/op 22 allocs/op
|
||||
BenchmarkTraffic_GPlus2Params 169714 7862 ns/op 2248 B/op 28 allocs/op
|
||||
BenchmarkVulcan_GPlus2Params 1222288 974 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_GPlusAll 845606 1398 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_GPlusAll 1000000 1009 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_GPlusAll 103830 11386 ns/op 5488 B/op 61 allocs/op
|
||||
BenchmarkBeego_GPlusAll 82653 14784 ns/op 4576 B/op 39 allocs/op
|
||||
BenchmarkBone_GPlusAll 36601 33123 ns/op 11744 B/op 109 allocs/op
|
||||
BenchmarkChi_GPlusAll 95264 12831 ns/op 5616 B/op 39 allocs/op
|
||||
BenchmarkDenco_GPlusAll 567681 2950 ns/op 672 B/op 11 allocs/op
|
||||
BenchmarkEcho_GPlusAll 720366 1665 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_GPlusAll 1000000 1185 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_GPlusAll 71575 16365 ns/op 8040 B/op 103 allocs/op
|
||||
BenchmarkGoji_GPlusAll 136352 9191 ns/op 3696 B/op 22 allocs/op
|
||||
BenchmarkGojiv2_GPlusAll 38006 31802 ns/op 17616 B/op 154 allocs/op
|
||||
BenchmarkGoJsonRest_GPlusAll 57238 21561 ns/op 8117 B/op 170 allocs/op
|
||||
BenchmarkGoRestful_GPlusAll 15147 79276 ns/op 55520 B/op 192 allocs/op
|
||||
BenchmarkGorillaMux_GPlusAll 24446 48410 ns/op 16112 B/op 128 allocs/op
|
||||
BenchmarkGowwwRouter_GPlusAll 150112 7770 ns/op 4752 B/op 33 allocs/op
|
||||
BenchmarkHttpRouter_GPlusAll 1367820 878 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_GPlusAll 166628 8004 ns/op 4032 B/op 38 allocs/op
|
||||
BenchmarkKocha_GPlusAll 265694 4570 ns/op 976 B/op 43 allocs/op
|
||||
BenchmarkLARS_GPlusAll 1000000 1068 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_GPlusAll 54564 23305 ns/op 9568 B/op 104 allocs/op
|
||||
BenchmarkMartini_GPlusAll 16274 73845 ns/op 14016 B/op 145 allocs/op
|
||||
BenchmarkPat_GPlusAll 27181 44478 ns/op 15264 B/op 271 allocs/op
|
||||
BenchmarkPossum_GPlusAll 122587 10277 ns/op 5408 B/op 39 allocs/op
|
||||
BenchmarkR2router_GPlusAll 130137 9297 ns/op 5040 B/op 63 allocs/op
|
||||
BenchmarkRivet_GPlusAll 532438 3323 ns/op 768 B/op 11 allocs/op
|
||||
BenchmarkTango_GPlusAll 86054 14531 ns/op 3656 B/op 104 allocs/op
|
||||
BenchmarkTigerTonic_GPlusAll 33936 35356 ns/op 11600 B/op 242 allocs/op
|
||||
BenchmarkTraffic_GPlusAll 17833 68181 ns/op 26248 B/op 341 allocs/op
|
||||
BenchmarkVulcan_GPlusAll 120109 9861 ns/op 1274 B/op 39 allocs/op
|
||||
```
|
||||
Route: `/:a/:b/.../:t` (20 segments) — Request: `GET /a/b/.../t`
|
||||
|
||||
## Parse.com
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | ----: | ----: | --------: |
|
||||
| 1 | **Gin** | 121.7 | 0 | 0 |
|
||||
| 2 | **Echo** | 127.5 | 0 | 0 |
|
||||
| 3 | **BunRouter** | 211.4 | 0 | 0 |
|
||||
| 4 | HttpRouter | 290.2 | 704 | 1 |
|
||||
| 5 | Fiber | 466.1 | 0 | 0 |
|
||||
| 6 | Goji v2 | 745.3 | 1,440 | 8 |
|
||||
| 7 | Beego | 1,099 | 352 | 3 |
|
||||
| 8 | Chi | 1,805 | 2,504 | 9 |
|
||||
| 9 | HttpTreeMux | 1,857 | 3,144 | 13 |
|
||||
| 10 | Macaron | 2,058 | 2,864 | 15 |
|
||||
| 11 | GorillaMux | 2,223 | 3,272 | 13 |
|
||||
| 12 | GoRestful | 3,337 | 7,008 | 20 |
|
||||
|
||||
```sh
|
||||
BenchmarkGin_ParseStatic 18877833 63.5 ns/op 0 B/op 0 allocs/op
|
||||
### Param Write
|
||||
|
||||
BenchmarkAce_ParseStatic 19663731 60.8 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_ParseStatic 28967341 41.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_ParseStatic 3006984 402 ns/op 120 B/op 3 allocs/op
|
||||
BenchmarkBeego_ParseStatic 1000000 1031 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_ParseStatic 1782482 675 ns/op 144 B/op 3 allocs/op
|
||||
BenchmarkChi_ParseStatic 1453261 819 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_ParseStatic 45023595 26.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkEcho_ParseStatic 17330470 69.3 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_ParseStatic 1644006 731 ns/op 296 B/op 5 allocs/op
|
||||
BenchmarkGoji_ParseStatic 7026930 170 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGojiv2_ParseStatic 517618 2037 ns/op 1312 B/op 10 allocs/op
|
||||
BenchmarkGoJsonRest_ParseStatic 1227080 975 ns/op 329 B/op 11 allocs/op
|
||||
BenchmarkGoRestful_ParseStatic 192458 6659 ns/op 4256 B/op 13 allocs/op
|
||||
BenchmarkGorillaMux_ParseStatic 744062 2109 ns/op 976 B/op 9 allocs/op
|
||||
BenchmarkGowwwRouter_ParseStatic 37781062 31.8 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpRouter_ParseStatic 45311223 26.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_ParseStatic 21383475 56.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkKocha_ParseStatic 29953290 40.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLARS_ParseStatic 20036196 62.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_ParseStatic 1000000 1740 ns/op 736 B/op 8 allocs/op
|
||||
BenchmarkMartini_ParseStatic 404156 3801 ns/op 768 B/op 9 allocs/op
|
||||
BenchmarkPat_ParseStatic 1547180 772 ns/op 240 B/op 5 allocs/op
|
||||
BenchmarkPossum_ParseStatic 1608991 757 ns/op 416 B/op 3 allocs/op
|
||||
BenchmarkR2router_ParseStatic 3177936 385 ns/op 144 B/op 4 allocs/op
|
||||
BenchmarkRivet_ParseStatic 17783205 67.4 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkTango_ParseStatic 1210777 990 ns/op 248 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_ParseStatic 5316440 231 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkTraffic_ParseStatic 496050 2539 ns/op 1256 B/op 19 allocs/op
|
||||
BenchmarkVulcan_ParseStatic 2462798 488 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_ParseParam 13393669 89.6 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_ParseParam 19836619 60.4 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_ParseParam 1405954 864 ns/op 467 B/op 5 allocs/op
|
||||
BenchmarkBeego_ParseParam 1000000 1065 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_ParseParam 1000000 1698 ns/op 896 B/op 7 allocs/op
|
||||
BenchmarkChi_ParseParam 1356037 873 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_ParseParam 6241392 204 ns/op 64 B/op 1 allocs/op
|
||||
BenchmarkEcho_ParseParam 14088100 85.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_ParseParam 17426064 68.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_ParseParam 1000000 1254 ns/op 664 B/op 8 allocs/op
|
||||
BenchmarkGoji_ParseParam 1682574 713 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_ParseParam 502224 2333 ns/op 1360 B/op 12 allocs/op
|
||||
BenchmarkGoJsonRest_ParseParam 1000000 1401 ns/op 649 B/op 13 allocs/op
|
||||
BenchmarkGoRestful_ParseParam 182623 7097 ns/op 4576 B/op 14 allocs/op
|
||||
BenchmarkGorillaMux_ParseParam 482332 2477 ns/op 1280 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_ParseParam 1834873 657 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_ParseParam 23593393 51.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_ParseParam 2100160 574 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkKocha_ParseParam 4837220 252 ns/op 56 B/op 3 allocs/op
|
||||
BenchmarkLARS_ParseParam 18411192 66.2 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_ParseParam 571870 2398 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_ParseParam 286262 4268 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkPat_ParseParam 692906 2157 ns/op 992 B/op 15 allocs/op
|
||||
BenchmarkPossum_ParseParam 1000000 1011 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_ParseParam 1722735 697 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_ParseParam 6058054 203 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkTango_ParseParam 1000000 1061 ns/op 280 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_ParseParam 890275 2277 ns/op 784 B/op 15 allocs/op
|
||||
BenchmarkTraffic_ParseParam 351322 3543 ns/op 1896 B/op 21 allocs/op
|
||||
BenchmarkVulcan_ParseParam 2076544 572 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_Parse2Params 11718074 101 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_Parse2Params 16264988 73.4 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_Parse2Params 1238322 973 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkBeego_Parse2Params 1000000 1120 ns/op 352 B/op 3 allocs/op
|
||||
BenchmarkBone_Parse2Params 1000000 1632 ns/op 848 B/op 6 allocs/op
|
||||
BenchmarkChi_Parse2Params 1239477 955 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkDenco_Parse2Params 4944133 245 ns/op 64 B/op 1 allocs/op
|
||||
BenchmarkEcho_Parse2Params 10518286 114 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_Parse2Params 14505195 82.7 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_Parse2Params 1000000 1437 ns/op 712 B/op 9 allocs/op
|
||||
BenchmarkGoji_Parse2Params 1689883 707 ns/op 336 B/op 2 allocs/op
|
||||
BenchmarkGojiv2_Parse2Params 502334 2308 ns/op 1344 B/op 11 allocs/op
|
||||
BenchmarkGoJsonRest_Parse2Params 1000000 1771 ns/op 713 B/op 14 allocs/op
|
||||
BenchmarkGoRestful_Parse2Params 159092 7583 ns/op 4928 B/op 14 allocs/op
|
||||
BenchmarkGorillaMux_Parse2Params 417548 2980 ns/op 1296 B/op 10 allocs/op
|
||||
BenchmarkGowwwRouter_Parse2Params 1751737 686 ns/op 432 B/op 3 allocs/op
|
||||
BenchmarkHttpRouter_Parse2Params 18089204 66.3 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_Parse2Params 1556986 777 ns/op 384 B/op 4 allocs/op
|
||||
BenchmarkKocha_Parse2Params 2493082 485 ns/op 128 B/op 5 allocs/op
|
||||
BenchmarkLARS_Parse2Params 15350108 78.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_Parse2Params 530974 2605 ns/op 1072 B/op 10 allocs/op
|
||||
BenchmarkMartini_Parse2Params 247069 4673 ns/op 1152 B/op 11 allocs/op
|
||||
BenchmarkPat_Parse2Params 816295 2126 ns/op 752 B/op 16 allocs/op
|
||||
BenchmarkPossum_Parse2Params 1000000 1002 ns/op 496 B/op 5 allocs/op
|
||||
BenchmarkR2router_Parse2Params 1569771 733 ns/op 432 B/op 5 allocs/op
|
||||
BenchmarkRivet_Parse2Params 4080546 295 ns/op 96 B/op 1 allocs/op
|
||||
BenchmarkTango_Parse2Params 1000000 1121 ns/op 312 B/op 8 allocs/op
|
||||
BenchmarkTigerTonic_Parse2Params 399556 3470 ns/op 1168 B/op 22 allocs/op
|
||||
BenchmarkTraffic_Parse2Params 314194 4159 ns/op 1944 B/op 22 allocs/op
|
||||
BenchmarkVulcan_Parse2Params 1827559 664 ns/op 98 B/op 3 allocs/op
|
||||
BenchmarkAce_ParseAll 478395 2503 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkAero_ParseAll 715392 1658 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBear_ParseAll 59191 20124 ns/op 8928 B/op 110 allocs/op
|
||||
BenchmarkBeego_ParseAll 45507 27266 ns/op 9152 B/op 78 allocs/op
|
||||
BenchmarkBone_ParseAll 29328 41459 ns/op 16208 B/op 147 allocs/op
|
||||
BenchmarkChi_ParseAll 48531 25053 ns/op 11232 B/op 78 allocs/op
|
||||
BenchmarkDenco_ParseAll 325532 4284 ns/op 928 B/op 16 allocs/op
|
||||
BenchmarkEcho_ParseAll 433771 2759 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGin_ParseAll 576316 2082 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGocraftWeb_ParseAll 41500 29692 ns/op 13728 B/op 181 allocs/op
|
||||
BenchmarkGoji_ParseAll 80833 15563 ns/op 5376 B/op 32 allocs/op
|
||||
BenchmarkGojiv2_ParseAll 19836 60335 ns/op 34448 B/op 277 allocs/op
|
||||
BenchmarkGoJsonRest_ParseAll 32210 38027 ns/op 13866 B/op 321 allocs/op
|
||||
BenchmarkGoRestful_ParseAll 6644 190842 ns/op 117600 B/op 354 allocs/op
|
||||
BenchmarkGorillaMux_ParseAll 12634 95894 ns/op 30288 B/op 250 allocs/op
|
||||
BenchmarkGowwwRouter_ParseAll 98152 12159 ns/op 6912 B/op 48 allocs/op
|
||||
BenchmarkHttpRouter_ParseAll 933208 1273 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkHttpTreeMux_ParseAll 107191 11554 ns/op 5728 B/op 51 allocs/op
|
||||
BenchmarkKocha_ParseAll 184862 6225 ns/op 1112 B/op 54 allocs/op
|
||||
BenchmarkLARS_ParseAll 644546 1858 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMacaron_ParseAll 26145 46484 ns/op 19136 B/op 208 allocs/op
|
||||
BenchmarkMartini_ParseAll 10000 121838 ns/op 25072 B/op 253 allocs/op
|
||||
BenchmarkPat_ParseAll 25417 47196 ns/op 15216 B/op 308 allocs/op
|
||||
BenchmarkPossum_ParseAll 58550 20735 ns/op 10816 B/op 78 allocs/op
|
||||
BenchmarkR2router_ParseAll 72732 16584 ns/op 8352 B/op 120 allocs/op
|
||||
BenchmarkRivet_ParseAll 281365 4968 ns/op 912 B/op 16 allocs/op
|
||||
BenchmarkTango_ParseAll 42831 28668 ns/op 7168 B/op 208 allocs/op
|
||||
BenchmarkTigerTonic_ParseAll 23774 49972 ns/op 16048 B/op 332 allocs/op
|
||||
BenchmarkTraffic_ParseAll 10000 104679 ns/op 45520 B/op 605 allocs/op
|
||||
BenchmarkVulcan_ParseAll 64810 18108 ns/op 2548 B/op 78 allocs/op
|
||||
```
|
||||
Route: `/user/:name` with response write — Request: `GET /user/gordon`
|
||||
|
||||
| Rank | Router | ns/op | B/op | allocs/op |
|
||||
| :--: | :------------ | ----: | ----: | --------: |
|
||||
| 1 | **BunRouter** | 25.86 | 0 | 0 |
|
||||
| 2 | **Gin** | 27.65 | 0 | 0 |
|
||||
| 3 | HttpRouter | 37.40 | 32 | 1 |
|
||||
| 4 | Echo | 47.94 | 8 | 1 |
|
||||
| 5 | Fiber | 125.7 | 0 | 0 |
|
||||
| 6 | HttpTreeMux | 180.4 | 352 | 3 |
|
||||
| 7 | Chi | 348.3 | 704 | 4 |
|
||||
| 8 | Beego | 386.1 | 360 | 4 |
|
||||
| 9 | Goji v2 | 516.9 | 1,168 | 10 |
|
||||
| 10 | GorillaMux | 665.5 | 1,152 | 8 |
|
||||
| 11 | Macaron | 784.3 | 1,112 | 13 |
|
||||
| 12 | GoRestful | 1,534 | 4,608 | 16 |
|
||||
|
||||
515
CHANGELOG.md
515
CHANGELOG.md
@ -1,284 +1,501 @@
|
||||
# Gin ChangeLog
|
||||
|
||||
## Gin v1.12.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat(render): add bson protocol ([#4145](https://github.com/gin-gonic/gin/pull/4145))
|
||||
- feat(context): add GetError and GetErrorSlice methods for error retrieval ([#4502](https://github.com/gin-gonic/gin/pull/4502))
|
||||
- feat(binding): add support for encoding.UnmarshalText in uri/query binding ([#4203](https://github.com/gin-gonic/gin/pull/4203))
|
||||
- feat(gin): add option to use escaped path ([#4420](https://github.com/gin-gonic/gin/pull/4420))
|
||||
- feat(context): add Protocol Buffers support to content negotiation ([#4423](https://github.com/gin-gonic/gin/pull/4423))
|
||||
- feat(context): implemented Delete method ([#38e7651](https://github.com/gin-gonic/gin/commit/38e7651))
|
||||
- feat(logger): color latency ([#4146](https://github.com/gin-gonic/gin/pull/4146))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- perf(tree): reduce allocations in findCaseInsensitivePath ([#4417](https://github.com/gin-gonic/gin/pull/4417))
|
||||
- perf(recovery): optimize line reading in stack function ([#4466](https://github.com/gin-gonic/gin/pull/4466))
|
||||
- perf(path): replace regex with custom functions in redirectTrailingSlash ([#4414](https://github.com/gin-gonic/gin/pull/4414))
|
||||
- perf(tree): optimize path parsing using strings.Count ([#4246](https://github.com/gin-gonic/gin/pull/4246))
|
||||
- chore(logger): allow skipping query string output ([#4547](https://github.com/gin-gonic/gin/pull/4547))
|
||||
- chore(context): always trust xff headers from unix socket ([#3359](https://github.com/gin-gonic/gin/pull/3359))
|
||||
- chore(response): prevent Flush() panic when the underlying ResponseWriter does not implement `http.Flusher` ([#4479](https://github.com/gin-gonic/gin/pull/4479))
|
||||
- refactor(recovery): smart error comparison ([#4142](https://github.com/gin-gonic/gin/pull/4142))
|
||||
- refactor(context): replace hardcoded localhost IPs with constants ([#4481](https://github.com/gin-gonic/gin/pull/4481))
|
||||
- refactor(utils): move util functions to utils.go ([#4467](https://github.com/gin-gonic/gin/pull/4467))
|
||||
- refactor(binding): use maps.Copy for cleaner map handling ([#4352](https://github.com/gin-gonic/gin/pull/4352))
|
||||
- refactor(context): using maps.Clone ([#4333](https://github.com/gin-gonic/gin/pull/4333))
|
||||
- refactor(ginS): use sync.OnceValue to simplify engine function ([#4314](https://github.com/gin-gonic/gin/pull/4314))
|
||||
- refactor: replace magic numbers with named constants in bodyAllowedForStatus ([#4529](https://github.com/gin-gonic/gin/pull/4529))
|
||||
- refactor: for loop can be modernized using range over int ([#4392](https://github.com/gin-gonic/gin/pull/4392))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath ([#4535](https://github.com/gin-gonic/gin/pull/4535))
|
||||
- fix(render): write content length in Data.Render ([#4206](https://github.com/gin-gonic/gin/pull/4206))
|
||||
- fix(context): ClientIP handling for multiple X-Forwarded-For header values ([#4472](https://github.com/gin-gonic/gin/pull/4472))
|
||||
- fix(binding): empty value error ([#2169](https://github.com/gin-gonic/gin/pull/2169))
|
||||
- fix(recover): suppress http.ErrAbortHandler in recover ([#4336](https://github.com/gin-gonic/gin/pull/4336))
|
||||
- fix(gin): literal colon routes not working with engine.Handler() ([#4415](https://github.com/gin-gonic/gin/pull/4415))
|
||||
- fix(gin): close os.File in RunFd to prevent resource leak ([#4422](https://github.com/gin-gonic/gin/pull/4422))
|
||||
- fix(response): refine hijack behavior for response lifecycle ([#4373](https://github.com/gin-gonic/gin/pull/4373))
|
||||
- fix(binding): improve empty slice/array handling in form binding ([#4380](https://github.com/gin-gonic/gin/pull/4380))
|
||||
- fix(debug): version mismatch ([#4403](https://github.com/gin-gonic/gin/pull/4403))
|
||||
- fix: correct typos, improve documentation clarity, and remove dead code ([#4511](https://github.com/gin-gonic/gin/pull/4511))
|
||||
|
||||
### Build process updates / CI
|
||||
|
||||
- ci: update Go version support to 1.25+ across CI and docs ([#4550](https://github.com/gin-gonic/gin/pull/4550))
|
||||
- chore(binding): upgrade bson dependency to mongo-driver v2 ([#4549](https://github.com/gin-gonic/gin/pull/4549))
|
||||
|
||||
## Gin v1.11.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210))
|
||||
- feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970))
|
||||
- feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904))
|
||||
- feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939))
|
||||
- feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190))
|
||||
- feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048))
|
||||
- feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339))
|
||||
- refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354))
|
||||
- refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353))
|
||||
- refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080))
|
||||
- refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910))
|
||||
- refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181))
|
||||
- refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066))
|
||||
- refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522))
|
||||
- refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068))
|
||||
- refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
|
||||
- refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
|
||||
- tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975))
|
||||
- context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987))
|
||||
- fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193))
|
||||
- fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003))
|
||||
- fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580))
|
||||
- fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
|
||||
- fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
|
||||
- fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413))
|
||||
- fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222))
|
||||
- fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077))
|
||||
|
||||
### Build process updates / CI
|
||||
|
||||
- ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359))
|
||||
- ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341))
|
||||
- build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342))
|
||||
- ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154))
|
||||
- build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960))
|
||||
- ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090))
|
||||
- ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126))
|
||||
|
||||
### Dependency updates
|
||||
|
||||
- chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356))
|
||||
- chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347))
|
||||
- chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351))
|
||||
- chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328))
|
||||
- chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221))
|
||||
- chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052))
|
||||
|
||||
### Documentation updates
|
||||
|
||||
- docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360))
|
||||
- docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207))
|
||||
- docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953))
|
||||
- docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997))
|
||||
- docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938))
|
||||
- docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020))
|
||||
- docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078))
|
||||
- docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079))
|
||||
|
||||
---
|
||||
|
||||
## Gin v1.10.1
|
||||
|
||||
### Features
|
||||
|
||||
- refactor: strengthen HTTPS security and improve code organization
|
||||
- feat(binding): Support custom BindUnmarshaler for binding. (#3933)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940)
|
||||
- chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941)
|
||||
- chore: update external dependencies to latest versions (#3950)
|
||||
- chore: update various Go dependencies to latest versions (#3901)
|
||||
- chore: refactor configuration files for better readability (#3951)
|
||||
- chore: update changelog categories and improve documentation (#3917)
|
||||
- feat: update version constant to v1.10.0 (#3952)
|
||||
|
||||
### Build process updates
|
||||
|
||||
- ci(release): refactor changelog regex patterns and exclusions (#3914)
|
||||
- ci(Makefile): vet command add .PHONY (#3915)
|
||||
|
||||
## Gin v1.10.0
|
||||
|
||||
### Features
|
||||
|
||||
- feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
|
||||
- feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
|
||||
- feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
|
||||
- feat(binding): support override default binding implement (#3514) (@ssfyn)
|
||||
- feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
|
||||
- feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
|
||||
- fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
|
||||
- fix(binding): dereference pointer to struct (#3199) (@echovl)
|
||||
- fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
|
||||
- fix(engine): fix unit test (#3878) (@flc1125)
|
||||
- fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
|
||||
- fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
|
||||
- fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
|
||||
- fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
|
||||
- fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
|
||||
- fix(uri): query binding bug (#3236) (@illiafox)
|
||||
- fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
|
||||
- fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- chore(CI): update release args (#3595) (@qloog)
|
||||
- chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
|
||||
- chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
|
||||
- chore(deps): update dependencies to latest versions (#3835) (@appleboy)
|
||||
- chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
|
||||
- chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
|
||||
- chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
|
||||
- chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
|
||||
- chore(refactor): modify interface check way (#3855) (@demoManito)
|
||||
- chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
|
||||
- chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
|
||||
- chore: refactor CI and update dependencies (#3848) (@appleboy)
|
||||
- chore: refactor configuration files for better readability (#3951) (@appleboy)
|
||||
- chore: update GitHub Actions configuration (#3792) (@appleboy)
|
||||
- chore: update changelog categories and improve documentation (#3917) (@appleboy)
|
||||
- chore: update dependencies to latest versions (#3694) (@appleboy)
|
||||
- chore: update external dependencies to latest versions (#3950) (@appleboy)
|
||||
- chore: update various Go dependencies to latest versions (#3901) (@appleboy)
|
||||
|
||||
### Build process updates
|
||||
|
||||
- build(codecov): Added a codecov configuration (#3891) (@flc1125)
|
||||
- ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
|
||||
- ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
|
||||
- ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
|
||||
- ci(testing): add go1.22 version (#3842) (@appleboy)
|
||||
|
||||
### Documentation updates
|
||||
|
||||
- docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
|
||||
- docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
|
||||
- docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
|
||||
- docs: fix typo in comment (#3868) (@testwill)
|
||||
- docs: fix typo in function documentation (#3872) (@TotomiEcio)
|
||||
- docs: remove redundant comments (#3765) (@WeiTheShinobi)
|
||||
- feat: update version constant to v1.10.0 (#3952) (@appleboy)
|
||||
|
||||
### Others
|
||||
|
||||
- Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
|
||||
- test(git): gitignore add develop tools (#3370) (@demoManito)
|
||||
- test(http): use constant instead of numeric literal (#3863) (@testwill)
|
||||
- test(path): Optimize unit test execution results (#3883) (@flc1125)
|
||||
- test(render): increased unit tests coverage (#3691) (@araujo88)
|
||||
|
||||
## Gin v1.9.1
|
||||
|
||||
### BUG FIXES
|
||||
### BUG FIXES
|
||||
|
||||
* fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
|
||||
- fix Request.Context() checks [#3512](https://github.com/gin-gonic/gin/pull/3512)
|
||||
|
||||
### SECURITY
|
||||
|
||||
* fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
|
||||
- fix lack of escaping of filename in Content-Disposition [#3556](https://github.com/gin-gonic/gin/pull/3556)
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
|
||||
* convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
|
||||
* improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
|
||||
- refactor: use bytes.ReplaceAll directly [#3455](https://github.com/gin-gonic/gin/pull/3455)
|
||||
- convert strings and slices using the officially recommended way [#3344](https://github.com/gin-gonic/gin/pull/3344)
|
||||
- improve render code coverage [#3525](https://github.com/gin-gonic/gin/pull/3525)
|
||||
|
||||
### DOCS
|
||||
|
||||
* docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
|
||||
* chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
|
||||
- docs: changed documentation link for trusted proxies [#3575](https://github.com/gin-gonic/gin/pull/3575)
|
||||
- chore: improve linting, testing, and GitHub Actions setup [#3583](https://github.com/gin-gonic/gin/pull/3583)
|
||||
|
||||
## Gin v1.9.0
|
||||
|
||||
### BREAK CHANGES
|
||||
|
||||
* Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
|
||||
- Stop useless panicking in context and render [#2150](https://github.com/gin-gonic/gin/pull/2150)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
|
||||
* fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
|
||||
* Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
|
||||
- fix(router): tree bug where loop index is not decremented. [#3460](https://github.com/gin-gonic/gin/pull/3460)
|
||||
- fix(context): panic on NegotiateFormat - index out of range [#3397](https://github.com/gin-gonic/gin/pull/3397)
|
||||
- Add escape logic for header [#3500](https://github.com/gin-gonic/gin/pull/3500) and [#3503](https://github.com/gin-gonic/gin/pull/3503)
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
|
||||
* fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
|
||||
- Fix the GO-2022-0969 and GO-2022-0288 vulnerabilities [#3333](https://github.com/gin-gonic/gin/pull/3333)
|
||||
- fix(security): vulnerability GO-2023-1571 [#3505](https://github.com/gin-gonic/gin/pull/3505)
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
|
||||
* chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
|
||||
* fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
|
||||
* remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
|
||||
* refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
|
||||
* console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
|
||||
* chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
|
||||
* chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
|
||||
* chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
|
||||
- feat: add sonic json support [#3184](https://github.com/gin-gonic/gin/pull/3184)
|
||||
- chore(file): Creates a directory named path [#3316](https://github.com/gin-gonic/gin/pull/3316)
|
||||
- fix: modify interface check way [#3327](https://github.com/gin-gonic/gin/pull/3327)
|
||||
- remove deprecated of package io/ioutil [#3395](https://github.com/gin-gonic/gin/pull/3395)
|
||||
- refactor: avoid calling strings.ToLower twice [#3343](https://github.com/gin-gonic/gin/pull/3433)
|
||||
- console logger HTTP status code bug fixed [#3453](https://github.com/gin-gonic/gin/pull/3453)
|
||||
- chore(yaml): upgrade dependency to v3 version [#3456](https://github.com/gin-gonic/gin/pull/3456)
|
||||
- chore(router): match method added to routergroup for multiple HTTP methods supporting [#3464](https://github.com/gin-gonic/gin/pull/3464)
|
||||
- chore(http): add support for go1.20 http.rwUnwrapper to gin.responseWriter [#3489](https://github.com/gin-gonic/gin/pull/3489)
|
||||
|
||||
### DOCS
|
||||
|
||||
* docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
|
||||
* docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
|
||||
* docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
|
||||
* docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
|
||||
- docs: update markdown format [#3260](https://github.com/gin-gonic/gin/pull/3260)
|
||||
- docs(readme): Add the TOML rendering example [#3400](https://github.com/gin-gonic/gin/pull/3400)
|
||||
- docs(readme): move more example to docs/doc.md [#3449](https://github.com/gin-gonic/gin/pull/3449)
|
||||
- docs: update markdown format [#3446](https://github.com/gin-gonic/gin/pull/3446)
|
||||
|
||||
## Gin v1.8.2
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix(route): redirectSlash bug ([#3227]((https://github.com/gin-gonic/gin/pull/3227)))
|
||||
* fix(engine): missing route params for CreateTestContext ([#2778]((https://github.com/gin-gonic/gin/pull/2778))) ([#2803]((https://github.com/gin-gonic/gin/pull/2803)))
|
||||
- fix(route): redirectSlash bug ([#3227](<(https://github.com/gin-gonic/gin/pull/3227)>))
|
||||
- fix(engine): missing route params for CreateTestContext ([#2778](<(https://github.com/gin-gonic/gin/pull/2778)>)) ([#2803](<(https://github.com/gin-gonic/gin/pull/2803)>))
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Fix the GO-2022-1144 vulnerability ([#3432]((https://github.com/gin-gonic/gin/pull/3432)))
|
||||
- Fix the GO-2022-1144 vulnerability ([#3432](<(https://github.com/gin-gonic/gin/pull/3432)>))
|
||||
|
||||
## Gin v1.8.1
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
|
||||
- feat(context): add ContextWithFallback feature flag [#3172](https://github.com/gin-gonic/gin/pull/3172)
|
||||
|
||||
## Gin v1.8.0
|
||||
|
||||
### BREAK CHANGES
|
||||
|
||||
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
|
||||
* gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
|
||||
- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967). Please replace `RemoteIP() (net.IP, bool)` with `RemoteIP() net.IP`
|
||||
- gin.Context with fallback value from gin.Context.Request.Context() [#2751](https://github.com/gin-gonic/gin/pull/2751)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
|
||||
* Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
|
||||
* Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
|
||||
- Fixed SetOutput() panics on go 1.17 [#2861](https://github.com/gin-gonic/gin/pull/2861)
|
||||
- Fix: wrong when wildcard follows named param [#2983](https://github.com/gin-gonic/gin/pull/2983)
|
||||
- Fix: missing sameSite when do context.reset() [#3123](https://github.com/gin-gonic/gin/pull/3123)
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
|
||||
* RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
|
||||
* Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
|
||||
* Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
|
||||
* Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
|
||||
* Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
|
||||
* Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
|
||||
* Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
|
||||
* Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
|
||||
* Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
|
||||
* Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
|
||||
* Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
|
||||
* Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
|
||||
* TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
|
||||
* Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
|
||||
* Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
|
||||
* Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
|
||||
* Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
|
||||
* Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
|
||||
* Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
|
||||
* Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
|
||||
* IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
|
||||
- Use Header() instead of deprecated HeaderMap [#2694](https://github.com/gin-gonic/gin/pull/2694)
|
||||
- RouterGroup.Handle regular match optimization of http method [#2685](https://github.com/gin-gonic/gin/pull/2685)
|
||||
- Add support go-json, another drop-in json replacement [#2680](https://github.com/gin-gonic/gin/pull/2680)
|
||||
- Use errors.New to replace fmt.Errorf will much better [#2707](https://github.com/gin-gonic/gin/pull/2707)
|
||||
- Use Duration.Truncate for truncating precision [#2711](https://github.com/gin-gonic/gin/pull/2711)
|
||||
- Get client IP when using Cloudflare [#2723](https://github.com/gin-gonic/gin/pull/2723)
|
||||
- Optimize code adjust [#2700](https://github.com/gin-gonic/gin/pull/2700/files)
|
||||
- Optimize code and reduce code cyclomatic complexity [#2737](https://github.com/gin-gonic/gin/pull/2737)
|
||||
- Improve sliceValidateError.Error performance [#2765](https://github.com/gin-gonic/gin/pull/2765)
|
||||
- Support custom struct tag [#2720](https://github.com/gin-gonic/gin/pull/2720)
|
||||
- Improve router group tests [#2787](https://github.com/gin-gonic/gin/pull/2787)
|
||||
- Fallback Context.Deadline() Context.Done() Context.Err() to Context.Request.Context() [#2769](https://github.com/gin-gonic/gin/pull/2769)
|
||||
- Some codes optimize [#2830](https://github.com/gin-gonic/gin/pull/2830) [#2834](https://github.com/gin-gonic/gin/pull/2834) [#2838](https://github.com/gin-gonic/gin/pull/2838) [#2837](https://github.com/gin-gonic/gin/pull/2837) [#2788](https://github.com/gin-gonic/gin/pull/2788) [#2848](https://github.com/gin-gonic/gin/pull/2848) [#2851](https://github.com/gin-gonic/gin/pull/2851) [#2701](https://github.com/gin-gonic/gin/pull/2701)
|
||||
- TrustedProxies: Add default IPv6 support and refactor [#2967](https://github.com/gin-gonic/gin/pull/2967)
|
||||
- Test(route): expose performRequest func [#3012](https://github.com/gin-gonic/gin/pull/3012)
|
||||
- Support h2c with prior knowledge [#1398](https://github.com/gin-gonic/gin/pull/1398)
|
||||
- Feat attachment filename support utf8 [#3071](https://github.com/gin-gonic/gin/pull/3071)
|
||||
- Feat: add StaticFileFS [#2749](https://github.com/gin-gonic/gin/pull/2749)
|
||||
- Feat(context): return GIN Context from Value method [#2825](https://github.com/gin-gonic/gin/pull/2825)
|
||||
- Feat: automatically SetMode to TestMode when run go test [#3139](https://github.com/gin-gonic/gin/pull/3139)
|
||||
- Add TOML bining for gin [#3081](https://github.com/gin-gonic/gin/pull/3081)
|
||||
- IPv6 add default trusted proxies [#3033](https://github.com/gin-gonic/gin/pull/3033)
|
||||
|
||||
### DOCS
|
||||
|
||||
* Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
|
||||
- Add note about nomsgpack tag to the readme [#2703](https://github.com/gin-gonic/gin/pull/2703)
|
||||
|
||||
## Gin v1.7.7
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
|
||||
* Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
|
||||
* Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
|
||||
* Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
|
||||
- Fixed X-Forwarded-For unsafe handling of CVE-2020-28483 [#2844](https://github.com/gin-gonic/gin/pull/2844), closed issue [#2862](https://github.com/gin-gonic/gin/issues/2862).
|
||||
- Tree: updated the code logic for `latestNode` [#2897](https://github.com/gin-gonic/gin/pull/2897), closed issue [#2894](https://github.com/gin-gonic/gin/issues/2894) [#2878](https://github.com/gin-gonic/gin/issues/2878).
|
||||
- Tree: fixed the misplacement of adding slashes [#2847](https://github.com/gin-gonic/gin/pull/2847), closed issue [#2843](https://github.com/gin-gonic/gin/issues/2843).
|
||||
- Tree: fixed tsr with mixed static and wildcard paths [#2924](https://github.com/gin-gonic/gin/pull/2924), closed issue [#2918](https://github.com/gin-gonic/gin/issues/2918).
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
|
||||
* TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
|
||||
- TrustedProxies: make it backward-compatible [#2887](https://github.com/gin-gonic/gin/pull/2887), closed issue [#2819](https://github.com/gin-gonic/gin/issues/2819).
|
||||
- TrustedPlatform: provide custom options for another CDN services [#2906](https://github.com/gin-gonic/gin/pull/2906).
|
||||
|
||||
### DOCS
|
||||
|
||||
* NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
|
||||
- NoMethod: added usage annotation ([#2832](https://github.com/gin-gonic/gin/pull/2832#issuecomment-929954463)).
|
||||
|
||||
## Gin v1.7.6
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* bump new release to fix v1.7.5 release error by using v1.7.4 codes.
|
||||
- bump new release to fix v1.7.5 release error by using v1.7.4 codes.
|
||||
|
||||
## Gin v1.7.4
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* bump new release to fix checksum mismatch
|
||||
- bump new release to fix checksum mismatch
|
||||
|
||||
## Gin v1.7.3
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
|
||||
- fix level 1 router match [#2767](https://github.com/gin-gonic/gin/issues/2767), [#2796](https://github.com/gin-gonic/gin/issues/2796)
|
||||
|
||||
## Gin v1.7.2
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
|
||||
- Fix conflict between param and exact path [#2706](https://github.com/gin-gonic/gin/issues/2706). Close issue [#2682](https://github.com/gin-gonic/gin/issues/2682) [#2696](https://github.com/gin-gonic/gin/issues/2696).
|
||||
|
||||
## Gin v1.7.1
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
|
||||
- fix: data race with trustedCIDRs from [#2674](https://github.com/gin-gonic/gin/issues/2674)([#2675](https://github.com/gin-gonic/gin/pull/2675))
|
||||
|
||||
## Gin v1.7.0
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
|
||||
* fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
|
||||
* fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
|
||||
- fix compile error from [#2572](https://github.com/gin-gonic/gin/pull/2572) ([#2600](https://github.com/gin-gonic/gin/pull/2600))
|
||||
- fix: print headers without Authorization header on broken pipe ([#2528](https://github.com/gin-gonic/gin/pull/2528))
|
||||
- fix(tree): reassign fullpath when register new node ([#2366](https://github.com/gin-gonic/gin/pull/2366))
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||
* chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
|
||||
* Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
|
||||
* chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
|
||||
* chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
|
||||
* Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
|
||||
* update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
|
||||
* remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
|
||||
* Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
|
||||
* Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
|
||||
* binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
|
||||
* Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
|
||||
* Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
|
||||
* update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
|
||||
* reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
|
||||
* implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
|
||||
* Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
|
||||
* chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
|
||||
* Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
|
||||
* basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
|
||||
* Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||
* feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
|
||||
- Support params and exact routes without creating conflicts ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||
- chore: improve render string performance ([#2365](https://github.com/gin-gonic/gin/pull/2365))
|
||||
- Sync route tree to httprouter latest code ([#2368](https://github.com/gin-gonic/gin/pull/2368))
|
||||
- chore: rename getQueryCache/getFormCache to initQueryCache/initFormCa ([#2375](https://github.com/gin-gonic/gin/pull/2375))
|
||||
- chore(performance): improve countParams ([#2378](https://github.com/gin-gonic/gin/pull/2378))
|
||||
- Remove some functions that have the same effect as the bytes package ([#2387](https://github.com/gin-gonic/gin/pull/2387))
|
||||
- update:SetMode function ([#2321](https://github.com/gin-gonic/gin/pull/2321))
|
||||
- remove an unused type SecureJSONPrefix ([#2391](https://github.com/gin-gonic/gin/pull/2391))
|
||||
- Add a redirect sample for POST method ([#2389](https://github.com/gin-gonic/gin/pull/2389))
|
||||
- Add CustomRecovery builtin middleware ([#2322](https://github.com/gin-gonic/gin/pull/2322))
|
||||
- binding: avoid 2038 problem on 32-bit architectures ([#2450](https://github.com/gin-gonic/gin/pull/2450))
|
||||
- Prevent panic in Context.GetQuery() when there is no Request ([#2412](https://github.com/gin-gonic/gin/pull/2412))
|
||||
- Add GetUint and GetUint64 method on gin.context ([#2487](https://github.com/gin-gonic/gin/pull/2487))
|
||||
- update content-disposition header to MIME-style ([#2512](https://github.com/gin-gonic/gin/pull/2512))
|
||||
- reduce allocs and improve the render `WriteString` ([#2508](https://github.com/gin-gonic/gin/pull/2508))
|
||||
- implement ".Unwrap() error" on Error type ([#2525](https://github.com/gin-gonic/gin/pull/2525)) ([#2526](https://github.com/gin-gonic/gin/pull/2526))
|
||||
- Allow bind with a map[string]string ([#2484](https://github.com/gin-gonic/gin/pull/2484))
|
||||
- chore: update tree ([#2371](https://github.com/gin-gonic/gin/pull/2371))
|
||||
- Support binding for slice/array obj [Rewrite] ([#2302](https://github.com/gin-gonic/gin/pull/2302))
|
||||
- basic auth: fix timing oracle ([#2609](https://github.com/gin-gonic/gin/pull/2609))
|
||||
- Add mixed param and non-param paths (port of httprouter[#329](https://github.com/gin-gonic/gin/pull/329)) ([#2663](https://github.com/gin-gonic/gin/pull/2663))
|
||||
- feat(engine): add trustedproxies and remoteIP ([#2632](https://github.com/gin-gonic/gin/pull/2632))
|
||||
|
||||
## Gin v1.6.3
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
|
||||
- Improve performance: Change `*sync.RWMutex` to `sync.RWMutex` in context. [#2351](https://github.com/gin-gonic/gin/pull/2351)
|
||||
|
||||
## Gin v1.6.2
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
|
||||
- fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
|
||||
- Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
|
||||
|
||||
## Gin v1.6.1
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
|
||||
- Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
|
||||
|
||||
## Gin v1.6.0
|
||||
|
||||
### BREAKING
|
||||
|
||||
* chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
|
||||
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
|
||||
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
|
||||
- chore(performance): Improve performance for adding RemoveExtraSlash flag [#2159](https://github.com/gin-gonic/gin/pull/2159)
|
||||
- drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
|
||||
- Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
|
||||
|
||||
### FEATURES
|
||||
|
||||
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
||||
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
|
||||
- add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
||||
- FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
|
||||
* Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
|
||||
* fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
|
||||
* Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
|
||||
* [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
|
||||
* Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
|
||||
- Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
|
||||
- Use json marshall in context json to fix breaking new line issue. Fixes #2209 [#2228](https://github.com/gin-gonic/gin/pull/2228)
|
||||
- fix accept incoming network connections [#2216](https://github.com/gin-gonic/gin/pull/2216)
|
||||
- Fixed a bug in the calculation of the maximum number of parameters [#2166](https://github.com/gin-gonic/gin/pull/2166)
|
||||
- [FIX] allow empty headers on DataFromReader [#2121](https://github.com/gin-gonic/gin/pull/2121)
|
||||
- Add mutex for protect Context.Keys map [#1391](https://github.com/gin-gonic/gin/pull/1391)
|
||||
|
||||
### ENHANCEMENTS
|
||||
|
||||
* Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
|
||||
* tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
|
||||
* tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
|
||||
* chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
|
||||
* path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
|
||||
* Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
|
||||
* Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
|
||||
* tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
|
||||
* tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
|
||||
* tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
|
||||
* use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
|
||||
* upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
|
||||
* Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
|
||||
* Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
|
||||
- Add mitigation for log injection [#2277](https://github.com/gin-gonic/gin/pull/2277)
|
||||
- tree: range over nodes values [#2229](https://github.com/gin-gonic/gin/pull/2229)
|
||||
- tree: remove duplicate assignment [#2222](https://github.com/gin-gonic/gin/pull/2222)
|
||||
- chore: upgrade go-isatty and json-iterator/go [#2215](https://github.com/gin-gonic/gin/pull/2215)
|
||||
- path: sync code with httprouter [#2212](https://github.com/gin-gonic/gin/pull/2212)
|
||||
- Use zero-copy approach to convert types between string and byte slice [#2206](https://github.com/gin-gonic/gin/pull/2206)
|
||||
- Reuse bytes when cleaning the URL paths [#2179](https://github.com/gin-gonic/gin/pull/2179)
|
||||
- tree: remove one else statement [#2177](https://github.com/gin-gonic/gin/pull/2177)
|
||||
- tree: sync httprouter update (#2173) (#2172) [#2171](https://github.com/gin-gonic/gin/pull/2171)
|
||||
- tree: sync part httprouter codes and reduce if/else [#2163](https://github.com/gin-gonic/gin/pull/2163)
|
||||
- use http method constant [#2155](https://github.com/gin-gonic/gin/pull/2155)
|
||||
- upgrade go-validator to v10 [#2149](https://github.com/gin-gonic/gin/pull/2149)
|
||||
- Refactor redirect request in gin.go [#1970](https://github.com/gin-gonic/gin/pull/1970)
|
||||
- Add build tag nomsgpack [#1852](https://github.com/gin-gonic/gin/pull/1852)
|
||||
|
||||
### DOCS
|
||||
|
||||
* docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
|
||||
* Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
|
||||
* Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
|
||||
* Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
|
||||
* Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
|
||||
* Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
|
||||
* upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
|
||||
* Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
|
||||
* Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
|
||||
* Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
|
||||
* docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
|
||||
* Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
|
||||
- docs(path): improve comments [#2223](https://github.com/gin-gonic/gin/pull/2223)
|
||||
- Renew README to fit the modification of SetCookie method [#2217](https://github.com/gin-gonic/gin/pull/2217)
|
||||
- Fix spelling [#2202](https://github.com/gin-gonic/gin/pull/2202)
|
||||
- Remove broken link from README. [#2198](https://github.com/gin-gonic/gin/pull/2198)
|
||||
- Update docs on Context.Done(), Context.Deadline() and Context.Err() [#2196](https://github.com/gin-gonic/gin/pull/2196)
|
||||
- Update validator to v10 [#2190](https://github.com/gin-gonic/gin/pull/2190)
|
||||
- upgrade go-validator to v10 for README [#2189](https://github.com/gin-gonic/gin/pull/2189)
|
||||
- Update to currently output [#2188](https://github.com/gin-gonic/gin/pull/2188)
|
||||
- Fix "Custom Validators" example [#2186](https://github.com/gin-gonic/gin/pull/2186)
|
||||
- Add project to README [#2165](https://github.com/gin-gonic/gin/pull/2165)
|
||||
- docs(benchmarks): for gin v1.5 [#2153](https://github.com/gin-gonic/gin/pull/2153)
|
||||
- Changed wording for clarity in README.md [#2122](https://github.com/gin-gonic/gin/pull/2122)
|
||||
|
||||
### MISC
|
||||
|
||||
* ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
|
||||
* chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
|
||||
* Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
|
||||
* fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
|
||||
- ci support go1.14 [#2262](https://github.com/gin-gonic/gin/pull/2262)
|
||||
- chore: upgrade depend version [#2231](https://github.com/gin-gonic/gin/pull/2231)
|
||||
- Drop support go1.10 [#2147](https://github.com/gin-gonic/gin/pull/2147)
|
||||
- fix comment in `mode.go` [#2129](https://github.com/gin-gonic/gin/pull/2129)
|
||||
|
||||
## Gin v1.5.0
|
||||
|
||||
@ -317,14 +534,14 @@
|
||||
|
||||
### Gin v1.4.0
|
||||
|
||||
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
|
||||
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
|
||||
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
||||
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
|
||||
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
|
||||
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
|
||||
- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
|
||||
- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
|
||||
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
|
||||
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
|
||||
- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
|
||||
- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
|
||||
- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
|
||||
@ -356,7 +573,7 @@
|
||||
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
|
||||
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
|
||||
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
|
||||
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
|
||||
- [FIX] LoadHTML\* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
|
||||
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
|
||||
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
|
||||
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
|
||||
@ -371,7 +588,6 @@
|
||||
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
|
||||
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
|
||||
|
||||
|
||||
## Gin v1.3.0
|
||||
|
||||
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
|
||||
@ -414,7 +630,7 @@
|
||||
- [FIX] Refactor render
|
||||
- [FIX] Reworked tests
|
||||
- [FIX] logger now supports cygwin
|
||||
- [FIX] Use X-Forwarded-For before X-Real-Ip
|
||||
- [FIX] Use X-Forwarded-For before X-Real-IP
|
||||
- [FIX] time.Time binding (#904)
|
||||
|
||||
## Gin 1.1.4
|
||||
@ -469,7 +685,6 @@
|
||||
- [FIX] Error implements the json.Marshaller interface
|
||||
- [FIX] MIT license in every file
|
||||
|
||||
|
||||
## Gin 1.0rc1 (May 22, 2015)
|
||||
|
||||
- [PERFORMANCE] Zero allocation router
|
||||
@ -513,7 +728,6 @@
|
||||
- [FIX] Hijacking http
|
||||
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
||||
|
||||
|
||||
## Gin 0.6 (Mar 9, 2015)
|
||||
|
||||
- [NEW] Support multipart/form-data
|
||||
@ -523,14 +737,12 @@
|
||||
- [FIX] Unsigned integers in binding
|
||||
- [FIX] Improve color logger
|
||||
|
||||
|
||||
## Gin 0.5 (Feb 7, 2015)
|
||||
|
||||
- [NEW] Content Negotiation
|
||||
- [FIX] Solved security bug that allow a client to spoof ip
|
||||
- [FIX] Fix unexported/ignored fields in binding
|
||||
|
||||
|
||||
## Gin 0.4 (Aug 21, 2014)
|
||||
|
||||
- [NEW] Development mode
|
||||
@ -539,7 +751,6 @@
|
||||
- [FIX] Deferring WriteHeader()
|
||||
- [FIX] Improved documentation for model binding
|
||||
|
||||
|
||||
## Gin 0.3 (Jul 18, 2014)
|
||||
|
||||
- [PERFORMANCE] Normal log and error log are printed in the same call.
|
||||
@ -557,8 +768,8 @@
|
||||
- [FIX] Renaming Context.Req to Context.Request
|
||||
- [FIX] Check application/x-www-form-urlencoded when parsing form
|
||||
|
||||
|
||||
## Gin 0.2b (Jul 08, 2014)
|
||||
|
||||
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
|
||||
- [NEW] Travis CI integration
|
||||
- [NEW] Completely new logger
|
||||
|
||||
@ -1,13 +1,41 @@
|
||||
## Contributing
|
||||
# Contributing
|
||||
|
||||
- With issues:
|
||||
- Use the search tool before opening a new issue.
|
||||
- Please provide source code and commit sha if you found a bug.
|
||||
We welcome both issue reports and pull requests! Please follow these guidelines to help maintainers respond effectively.
|
||||
|
||||
## Issues
|
||||
|
||||
- **Before opening a new issue:**
|
||||
- Use the search tool to check for existing issues or feature requests.
|
||||
- Review existing issues and provide feedback or react to them.
|
||||
- Use English for all communications — it is the language all maintainers read and write.
|
||||
- For questions, configuration or deployment problems, please use the [Discussions Forum](https://github.com/gin-gonic/gin/discussions).
|
||||
- For bug reports involving sensitive security issues, email <appleboy.tw@gmail.com> instead of posting publicly.
|
||||
|
||||
- With pull requests:
|
||||
- Open your pull request against `master`
|
||||
- Your pull request should have no more than two commits, if not you should squash them.
|
||||
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
|
||||
- You should add/modify tests to cover your proposed code changes.
|
||||
- If your pull request contains a new feature, please document it on the README.
|
||||
- **Reporting a bug:**
|
||||
- Please provide a clear description of your issue, and a minimal reproducible code example if possible.
|
||||
- Include the Gin version (or commit reference), Go version, and operating system.
|
||||
- Indicate whether you can reproduce the bug and describe steps to do so.
|
||||
- Attach relevant logs per [Logging Documentation](https://docs.gitea.com/administration/logging-config#collecting-logs-for-help).
|
||||
|
||||
- **Feature requests:**
|
||||
- Before opening a request, check that a similar idea 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!
|
||||
|
||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Manuel Martínez-Almeida
|
||||
Copyright (c) 2014-present Manuel Martínez-Almeida
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
50
Makefile
50
Makefile
@ -4,14 +4,19 @@ GO_VERSION=$(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
PACKAGES ?= $(shell $(GO) list ./...)
|
||||
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
|
||||
GOFILES := $(shell find . -name "*.go")
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|ginS$$|binding$$|render$$' | grep -v examples)
|
||||
TESTTAGS ?= ""
|
||||
|
||||
.PHONY: test
|
||||
# Run tests to verify code functionality.
|
||||
test:
|
||||
echo "mode: count" > coverage.out
|
||||
for d in $(TESTFOLDER); do \
|
||||
$(GO) test $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||
if [ -n "$(TESTTAGS)" ]; then \
|
||||
$(GO) test $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||
else \
|
||||
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||
fi; \
|
||||
cat tmp.out; \
|
||||
if grep -q "^--- FAIL" tmp.out; then \
|
||||
rm tmp.out; \
|
||||
@ -30,10 +35,12 @@ test:
|
||||
done
|
||||
|
||||
.PHONY: fmt
|
||||
# Ensure consistent code formatting.
|
||||
fmt:
|
||||
$(GOFMT) -w $(GOFILES)
|
||||
|
||||
.PHONY: fmt-check
|
||||
# format (check only).
|
||||
fmt-check:
|
||||
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
@ -43,31 +50,36 @@ fmt-check:
|
||||
fi;
|
||||
|
||||
.PHONY: vet
|
||||
# Examine packages and report suspicious constructs if any.
|
||||
vet:
|
||||
$(GO) vet $(VETPACKAGES)
|
||||
|
||||
.PHONY: lint
|
||||
# Inspect source code for stylistic errors or potential bugs.
|
||||
lint:
|
||||
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) get -u golang.org/x/lint/golint; \
|
||||
fi
|
||||
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||
|
||||
.PHONY: misspell-check
|
||||
misspell-check:
|
||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
misspell -error $(GOFILES)
|
||||
|
||||
.PHONY: misspell
|
||||
# Correct commonly misspelled English words in source code.
|
||||
misspell:
|
||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
misspell -w $(GOFILES)
|
||||
|
||||
.PHONY: misspell-check
|
||||
# misspell (check only).
|
||||
misspell-check:
|
||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
misspell -error $(GOFILES)
|
||||
|
||||
.PHONY: tools
|
||||
# Install tools (golint and misspell).
|
||||
tools:
|
||||
@if [ $(GO_VERSION) -gt 15 ]; then \
|
||||
$(GO) install golang.org/x/lint/golint@latest; \
|
||||
@ -76,3 +88,23 @@ tools:
|
||||
$(GO) install golang.org/x/lint/golint; \
|
||||
$(GO) install github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
|
||||
.PHONY: help
|
||||
# Help.
|
||||
help:
|
||||
@echo ''
|
||||
@echo 'Usage:'
|
||||
@echo ' make [target]'
|
||||
@echo ''
|
||||
@echo 'Targets:'
|
||||
@awk '/^[a-zA-Z\-\0-9]+:/ { \
|
||||
helpMessage = match(lastLine, /^# (.*)/); \
|
||||
if (helpMessage) { \
|
||||
helpCommand = substr($$1, 0, index($$1, ":")-1); \
|
||||
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
|
||||
printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
|
||||
} \
|
||||
} \
|
||||
{ lastLine = $$0 }' $(MAKEFILE_LIST)
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
189
README.md
189
README.md
@ -2,121 +2,153 @@
|
||||
|
||||
<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://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://www.codetriage.com/gin-gonic/gin)
|
||||
[](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). If you need performance and good productivity, you will love Gin.
|
||||
## 📰 Gin 1.12.0 is now available!
|
||||
|
||||
**The key features of Gin are:**
|
||||
We're excited to announce the release of **[Gin 1.12.0](https://gin-gonic.com/en/blog/news/gin-1-12-0-release-announcement/)**! This release brings new features, performance improvements, and important bug fixes. Check out the [release announcement](https://gin-gonic.com/en/blog/news/gin-1-12-0-release-announcement/) on our official blog for the full details.
|
||||
|
||||
- Zero allocation router
|
||||
- Fast
|
||||
- Middleware support
|
||||
- Crash-free
|
||||
- JSON validation
|
||||
- Routes grouping
|
||||
- Error management
|
||||
- Rendering built-in
|
||||
- Extendable
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## Getting started
|
||||
**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
|
||||
|
||||
- **[Go](https://go.dev/)**: any one of the **three latest major** [releases](https://go.dev/doc/devel/release) (we test it with these).
|
||||
- **Go version**: Gin requires [Go](https://go.dev/) version [1.25](https://go.dev/doc/devel/release#go1.25.0) or above
|
||||
- **Basic Go knowledge**: Familiarity with Go syntax and package management is helpful
|
||||
|
||||
### Getting Gin
|
||||
### Installation
|
||||
|
||||
With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import
|
||||
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:
|
||||
|
||||
```
|
||||
```go
|
||||
import "github.com/gin-gonic/gin"
|
||||
```
|
||||
|
||||
to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies.
|
||||
### Your First Gin Application
|
||||
|
||||
Otherwise, run the following Go command to install the `gin` package:
|
||||
|
||||
```sh
|
||||
$ go get -u github.com/gin-gonic/gin
|
||||
```
|
||||
|
||||
### Running Gin
|
||||
|
||||
First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`:
|
||||
Here's a complete example that demonstrates Gin's simplicity:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a Gin router with default middleware (logger and recovery)
|
||||
r := gin.Default()
|
||||
|
||||
// Define a simple GET endpoint
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
// Return JSON response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
|
||||
// Start server on port 8080 (default)
|
||||
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
|
||||
if err := r.Run(); err != nil {
|
||||
log.Fatalf("failed to run server: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And use the Go command to run the demo:
|
||||
**Running the application:**
|
||||
|
||||
```
|
||||
# run example.go and visit 0.0.0.0:8080/ping on browser
|
||||
$ go run example.go
|
||||
```
|
||||
1. Save the code above as `main.go`
|
||||
2. Run the application:
|
||||
|
||||
### Learn more examples
|
||||
```sh
|
||||
go run main.go
|
||||
```
|
||||
|
||||
#### Quick Start
|
||||
3. Open your browser and visit [`http://localhost:8080/ping`](http://localhost:8080/ping)
|
||||
4. You should see: `{"message":"pong"}`
|
||||
|
||||
Learn and practice more examples, please read the [Gin Quick Start](docs/doc.md) which includes API examples and builds tag.
|
||||
**What this example demonstrates:**
|
||||
|
||||
#### Examples
|
||||
- Creating a Gin router with default middleware
|
||||
- Defining HTTP endpoints with simple handler functions
|
||||
- Returning JSON responses
|
||||
- Starting an HTTP server
|
||||
|
||||
A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository.
|
||||
### Next Steps
|
||||
|
||||
After running your first Gin application, explore these resources to learn more:
|
||||
|
||||
## Documentation
|
||||
#### 📚 Learning Resources
|
||||
|
||||
See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package.
|
||||
- **[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
|
||||
|
||||
All documentation is available on the Gin website.
|
||||
## 📖 Documentation
|
||||
|
||||
- [English](https://gin-gonic.com/docs/)
|
||||
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
|
||||
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
|
||||
- [日本語](https://gin-gonic.com/ja/docs/)
|
||||
- [Español](https://gin-gonic.com/es/docs/)
|
||||
- [한국어](https://gin-gonic.com/ko-kr/docs/)
|
||||
- [Turkish](https://gin-gonic.com/tr/docs/)
|
||||
- [Persian](https://gin-gonic.com/fa/docs/)
|
||||
### API Reference
|
||||
|
||||
### Articles about Gin
|
||||
- **[Go.dev API Documentation](https://pkg.go.dev/github.com/gin-gonic/gin)** - Complete API reference with examples
|
||||
|
||||
A curated list of awesome Gin framework.
|
||||
### User Guides
|
||||
|
||||
- [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
|
||||
The comprehensive documentation is available on [gin-gonic.com](https://gin-gonic.com) in multiple languages:
|
||||
|
||||
## Benchmarks
|
||||
- [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/)
|
||||
|
||||
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks details](/BENCHMARKS.md).
|
||||
### 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) |
|
||||
| ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
|
||||
| ------------------------------ | --------: | --------------: | -----------: | --------------: |
|
||||
| BenchmarkGin_GithubAll | **43550** | **27364 ns/op** | **0 B/op** | **0 allocs/op** |
|
||||
| BenchmarkAce_GithubAll | 40543 | 29670 ns/op | 0 B/op | 0 allocs/op |
|
||||
| BenchmarkAero_GithubAll | 57632 | 20648 ns/op | 0 B/op | 0 allocs/op |
|
||||
@ -153,26 +185,43 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
|
||||
- (3): Heap Memory (B/op), lower is better
|
||||
- (4): Average Allocations per Repetition (allocs/op), lower is better
|
||||
|
||||
## 🔌 Middleware Ecosystem
|
||||
|
||||
## Middlewares
|
||||
Gin has a rich ecosystem of middleware for common web development needs. Explore community-contributed middleware:
|
||||
|
||||
You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
|
||||
- **[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
|
||||
|
||||
## 🏢 Production Usage
|
||||
|
||||
## Users
|
||||
Gin powers many high-traffic applications and services in production:
|
||||
|
||||
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
|
||||
- **[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
|
||||
|
||||
* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
|
||||
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
|
||||
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
|
||||
* [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middlewares.
|
||||
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
|
||||
* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
|
||||
## 🤝 Contributing
|
||||
|
||||
Gin is the work of hundreds of contributors from around the world. We welcome and appreciate your contributions! See the full list of [contributors](https://github.com/gin-gonic/gin/graphs/contributors).
|
||||
|
||||
## Contributing
|
||||
### How to Contribute
|
||||
|
||||
Gin is the work of hundreds of contributors. We appreciate your help!
|
||||
- 🐛 **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
|
||||
|
||||
Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
|
||||
### 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!**
|
||||
|
||||
10
auth_test.go
10
auth_test.go
@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/login", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
|
||||
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/login", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/login", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@ -147,7 +147,7 @@ func TestBasicAuthForProxySucceed(t *testing.T) {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
@ -166,7 +166,7 @@ func TestBasicAuthForProxy407(t *testing.T) {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
|
||||
@ -14,21 +14,21 @@ import (
|
||||
func BenchmarkOneRoute(B *testing.B) {
|
||||
router := New()
|
||||
router.GET("/ping", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/ping")
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
}
|
||||
|
||||
func BenchmarkRecoveryMiddleware(B *testing.B) {
|
||||
router := New()
|
||||
router.Use(Recovery())
|
||||
router.GET("/", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/")
|
||||
runRequest(B, router, http.MethodGet, "/")
|
||||
}
|
||||
|
||||
func BenchmarkLoggerMiddleware(B *testing.B) {
|
||||
router := New()
|
||||
router.Use(LoggerWithWriter(newMockWriter()))
|
||||
router.GET("/", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/")
|
||||
runRequest(B, router, http.MethodGet, "/")
|
||||
}
|
||||
|
||||
func BenchmarkManyHandlers(B *testing.B) {
|
||||
@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
|
||||
router.Use(func(c *Context) {})
|
||||
router.Use(func(c *Context) {})
|
||||
router.GET("/ping", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/ping")
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
}
|
||||
|
||||
func Benchmark5Params(B *testing.B) {
|
||||
@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
|
||||
router := New()
|
||||
router.Use(func(c *Context) {})
|
||||
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/param/path/to/parameter/john/12345")
|
||||
runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345")
|
||||
}
|
||||
|
||||
func BenchmarkOneRouteJSON(B *testing.B) {
|
||||
@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
|
||||
router.GET("/json", func(c *Context) {
|
||||
c.JSON(http.StatusOK, data)
|
||||
})
|
||||
runRequest(B, router, "GET", "/json")
|
||||
runRequest(B, router, http.MethodGet, "/json")
|
||||
}
|
||||
|
||||
func BenchmarkOneRouteHTML(B *testing.B) {
|
||||
@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
|
||||
router.GET("/html", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "index", "hola")
|
||||
})
|
||||
runRequest(B, router, "GET", "/html")
|
||||
runRequest(B, router, http.MethodGet, "/html")
|
||||
}
|
||||
|
||||
func BenchmarkOneRouteSet(B *testing.B) {
|
||||
@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
|
||||
router.GET("/ping", func(c *Context) {
|
||||
c.Set("key", "value")
|
||||
})
|
||||
runRequest(B, router, "GET", "/ping")
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
}
|
||||
|
||||
func BenchmarkOneRouteString(B *testing.B) {
|
||||
@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
|
||||
router.GET("/text", func(c *Context) {
|
||||
c.String(http.StatusOK, "this is a plain text")
|
||||
})
|
||||
runRequest(B, router, "GET", "/text")
|
||||
runRequest(B, router, http.MethodGet, "/text")
|
||||
}
|
||||
|
||||
func BenchmarkManyRoutesFist(B *testing.B) {
|
||||
func BenchmarkManyRoutesFirst(B *testing.B) {
|
||||
router := New()
|
||||
router.Any("/ping", func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/ping")
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
}
|
||||
|
||||
func BenchmarkManyRoutesLast(B *testing.B) {
|
||||
@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
|
||||
router := New()
|
||||
router.Any("/something", func(c *Context) {})
|
||||
router.NoRoute(func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/ping")
|
||||
runRequest(B, router, http.MethodGet, "/ping")
|
||||
}
|
||||
|
||||
func Benchmark404Many(B *testing.B) {
|
||||
@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
|
||||
router.GET("/user/:id/:mode", func(c *Context) {})
|
||||
|
||||
router.NoRoute(func(c *Context) {})
|
||||
runRequest(B, router, "GET", "/viewfake")
|
||||
runRequest(B, router, http.MethodGet, "/viewfake")
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) {
|
||||
w := newMockWriter()
|
||||
B.ReportAllocs()
|
||||
B.ResetTimer()
|
||||
for i := 0; i < B.N; i++ {
|
||||
for B.Loop() {
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -84,7 +85,9 @@ var (
|
||||
YAML BindingBody = yamlBinding{}
|
||||
Uri BindingUri = uriBinding{}
|
||||
Header Binding = headerBinding{}
|
||||
Plain BindingBody = plainBinding{}
|
||||
TOML BindingBody = tomlBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -109,6 +112,8 @@ func Default(method, contentType string) Binding {
|
||||
return TOML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
@ -8,9 +8,11 @@ package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
@ -24,7 +26,7 @@ func TestBindingMsgPack(t *testing.T) {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err := codec.NewEncoder(buf, h).Encode(test)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := buf.Bytes()
|
||||
|
||||
@ -38,20 +40,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
|
||||
assert.Equal(t, name, b.Name())
|
||||
|
||||
obj := FooStruct{}
|
||||
req := requestWithBody("POST", path, body)
|
||||
req := requestWithBody(http.MethodPost, path, body)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err := b.Bind(req, &obj)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", obj.Foo)
|
||||
|
||||
obj = FooStruct{}
|
||||
req = requestWithBody("POST", badPath, badBody)
|
||||
req = requestWithBody(http.MethodPost, badPath, badBody)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err = MsgPack.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBindingDefaultMsgPack(t *testing.T) {
|
||||
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
|
||||
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
|
||||
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK))
|
||||
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2))
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ const (
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEYAML2 = "application/yaml"
|
||||
MIMETOML = "application/toml"
|
||||
MIMEBSON = "application/bson"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -81,6 +82,8 @@ var (
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
TOML = tomlBinding{}
|
||||
Plain = plainBinding{}
|
||||
BSON BindingBody = bsonBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -103,6 +106,8 @@ func Default(method, contentType string) Binding {
|
||||
return FormMultipart
|
||||
case MIMETOML:
|
||||
return TOML
|
||||
case MIMEBSON:
|
||||
return BSON
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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)
|
||||
}
|
||||
@ -5,8 +5,8 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -22,25 +22,20 @@ type SliceValidationError []error
|
||||
|
||||
// Error concatenates all error elements in SliceValidationError into a single string separated by \n.
|
||||
func (err SliceValidationError) Error() string {
|
||||
n := len(err)
|
||||
switch n {
|
||||
case 0:
|
||||
if len(err) == 0 {
|
||||
return ""
|
||||
default:
|
||||
var b strings.Builder
|
||||
if err[0] != nil {
|
||||
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
|
||||
}
|
||||
if n > 1 {
|
||||
for i := 1; i < n; i++ {
|
||||
if err[i] != nil {
|
||||
b.WriteString("\n")
|
||||
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for i := range len(err) {
|
||||
if err[i] != nil {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var _ StructValidator = (*defaultValidator)(nil)
|
||||
@ -63,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error {
|
||||
case reflect.Slice, reflect.Array:
|
||||
count := value.Len()
|
||||
validateRet := make(SliceValidationError, 0)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := range count {
|
||||
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
|
||||
validateRet = append(validateRet, err)
|
||||
}
|
||||
|
||||
@ -12,11 +12,14 @@ import (
|
||||
|
||||
func BenchmarkSliceValidationError(b *testing.B) {
|
||||
const size int = 100
|
||||
for i := 0; i < b.N; i++ {
|
||||
e := make(SliceValidationError, size)
|
||||
for j := 0; j < size; j++ {
|
||||
e[j] = errors.New(strconv.Itoa(j))
|
||||
}
|
||||
e := make(SliceValidationError, size)
|
||||
for j := 0; j < size; j++ {
|
||||
e[j] = errors.New(strconv.Itoa(j))
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if len(e.Error()) == 0 {
|
||||
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 zero elements", SliceValidationError{}, ""},
|
||||
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
|
||||
{"has two elements",
|
||||
{
|
||||
"has two elements",
|
||||
SliceValidationError{
|
||||
errors.New("first error"),
|
||||
errors.New("second error"),
|
||||
},
|
||||
"[0]: first error\n[1]: second error",
|
||||
},
|
||||
{"has many elements",
|
||||
{
|
||||
"has many elements",
|
||||
SliceValidationError{
|
||||
errors.New("first error"),
|
||||
errors.New("second error"),
|
||||
|
||||
@ -11,9 +11,11 @@ import (
|
||||
|
||||
const defaultMemory = 32 << 20
|
||||
|
||||
type formBinding struct{}
|
||||
type formPostBinding struct{}
|
||||
type formMultipartBinding struct{}
|
||||
type (
|
||||
formBinding struct{}
|
||||
formPostBinding struct{}
|
||||
formMultipartBinding struct{}
|
||||
)
|
||||
|
||||
func (formBinding) Name() string {
|
||||
return "form"
|
||||
|
||||
@ -5,16 +5,18 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -117,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
tValue := value.Type()
|
||||
|
||||
var isSet bool
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
for i := range value.NumField() {
|
||||
sf := tValue.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous { // unexported
|
||||
continue
|
||||
@ -136,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
type setOptions struct {
|
||||
isDefaultExists bool
|
||||
defaultValue string
|
||||
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
|
||||
parser string
|
||||
}
|
||||
|
||||
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
||||
@ -159,6 +163,16 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
||||
if k, v := head(opt, "="); k == "default" {
|
||||
setOpt.isDefaultExists = true
|
||||
setOpt.defaultValue = v
|
||||
|
||||
// convert semicolon-separated default values to csv-separated values for processing in setByForm
|
||||
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
|
||||
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
||||
}
|
||||
}
|
||||
} else if k, v = head(opt, "="); k == "parser" {
|
||||
setOpt.parser = v
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +181,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
||||
|
||||
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||
type BindUnmarshaler interface {
|
||||
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||
// UnmarshalParam decodes and assigns a value from a form or query param.
|
||||
UnmarshalParam(param string) error
|
||||
}
|
||||
|
||||
@ -182,6 +196,52 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
|
||||
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
|
||||
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
|
||||
switch parser {
|
||||
case "encoding.TextUnmarshaler":
|
||||
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return true, v.UnmarshalText([]byte(val))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
return vs, nil
|
||||
}
|
||||
|
||||
var sep string
|
||||
switch cfTag {
|
||||
case "csv":
|
||||
sep = ","
|
||||
case "ssv":
|
||||
sep = " "
|
||||
case "tsv":
|
||||
sep = "\t"
|
||||
case "pipes":
|
||||
sep = "|"
|
||||
default:
|
||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
|
||||
}
|
||||
|
||||
totalLength := 0
|
||||
for _, v := range vs {
|
||||
totalLength += strings.Count(v, sep) + 1
|
||||
}
|
||||
newVs = make([]string, 0, totalLength)
|
||||
for _, v := range vs {
|
||||
newVs = append(newVs, strings.Split(v, sep)...)
|
||||
}
|
||||
|
||||
return newVs, nil
|
||||
}
|
||||
|
||||
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
|
||||
vs, ok := form[tagValue]
|
||||
if !ok && !opt.isDefaultExists {
|
||||
@ -190,35 +250,89 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Slice:
|
||||
if !ok {
|
||||
if len(vs) == 0 {
|
||||
if !opt.isDefaultExists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
vs = []string{opt.defaultValue}
|
||||
// pre-process the default value for multi if present
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
vs = strings.Split(opt.defaultValue, ",")
|
||||
}
|
||||
}
|
||||
return true, setSlice(vs, value, field)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if vs, err = trySplit(vs, field); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, setSlice(vs, value, field, opt)
|
||||
case reflect.Array:
|
||||
if !ok {
|
||||
if len(vs) == 0 {
|
||||
if !opt.isDefaultExists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
vs = []string{opt.defaultValue}
|
||||
// pre-process the default value for multi if present
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
vs = strings.Split(opt.defaultValue, ",")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if vs, err = trySplit(vs, field); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(vs) != value.Len() {
|
||||
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||
}
|
||||
return true, setArray(vs, value, field)
|
||||
|
||||
return true, setArray(vs, value, field, opt)
|
||||
default:
|
||||
var val string
|
||||
if !ok {
|
||||
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||
val = opt.defaultValue
|
||||
}
|
||||
|
||||
if len(vs) > 0 {
|
||||
} else if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
}
|
||||
if ok, err := trySetCustom(val, value); ok {
|
||||
|
||||
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(val, value); ok {
|
||||
return ok, err
|
||||
}
|
||||
return true, setWithProperType(val, value, field)
|
||||
return true, setWithProperType(val, value, field, opt)
|
||||
}
|
||||
}
|
||||
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
|
||||
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
|
||||
return err
|
||||
} else if ok, err = trySetCustom(val, value); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||
if value.Kind() != reflect.String {
|
||||
val = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, value)
|
||||
@ -259,14 +373,14 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
case multipart.FileHeader:
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Map:
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Ptr:
|
||||
if !value.Elem().IsValid() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
}
|
||||
return setWithProperType(val, value.Elem(), field)
|
||||
return setWithProperType(val, value.Elem(), field, opt)
|
||||
default:
|
||||
return errUnknownType
|
||||
}
|
||||
@ -323,28 +437,34 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
timeFormat = time.RFC3339
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixnano":
|
||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := time.Duration(1)
|
||||
if tf == "unixnano" {
|
||||
d = time.Second
|
||||
var t time.Time
|
||||
switch tf {
|
||||
case "unix":
|
||||
t = time.Unix(tv, 0)
|
||||
case "unixmilli":
|
||||
t = time.UnixMilli(tv)
|
||||
case "unixmicro":
|
||||
t = time.UnixMicro(tv)
|
||||
default:
|
||||
t = time.Unix(0, tv)
|
||||
}
|
||||
|
||||
t := time.Unix(tv/int64(d), tv%int64(d))
|
||||
value.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
l := time.Local
|
||||
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
||||
l = time.UTC
|
||||
@ -367,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
for i, s := range vals {
|
||||
err := setWithProperType(s, value.Index(i), field)
|
||||
err := setWithProperType(s, value.Index(i), field, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -377,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||
err := setArray(vals, slice, field)
|
||||
err := setArray(vals, slice, field, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -388,6 +508,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
|
||||
}
|
||||
|
||||
func setTimeDuration(val string, value reflect.Value) error {
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -397,11 +521,8 @@ func setTimeDuration(val string, value reflect.Value) error {
|
||||
}
|
||||
|
||||
func head(str, sep string) (head string, tail string) {
|
||||
idx := strings.Index(str, sep)
|
||||
if idx < 0 {
|
||||
return str, ""
|
||||
}
|
||||
return str[:idx], str[idx+len(sep):]
|
||||
head, tail, _ = strings.Cut(str, sep)
|
||||
return head, tail
|
||||
}
|
||||
|
||||
func setFormMap(ptr any, form map[string][]string) error {
|
||||
@ -412,9 +533,7 @@ func setFormMap(ptr any, form map[string][]string) error {
|
||||
if !ok {
|
||||
return ErrConvertMapStringSlice
|
||||
}
|
||||
for k, v := range form {
|
||||
ptrMap[k] = v
|
||||
}
|
||||
maps.Copy(ptrMap, form)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ type structFull struct {
|
||||
|
||||
func BenchmarkMapFormFull(b *testing.B) {
|
||||
var s structFull
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
err := mapForm(&s, form)
|
||||
if err != nil {
|
||||
b.Fatalf("Error on a form mapping")
|
||||
@ -54,7 +54,7 @@ type structName struct {
|
||||
|
||||
func BenchmarkMapFormName(b *testing.B) {
|
||||
var s structName
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
err := mapForm(&s, form)
|
||||
if err != nil {
|
||||
b.Fatalf("Error on a form mapping")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@ func (headerBinding) Name() string {
|
||||
}
|
||||
|
||||
func (headerBinding) Bind(req *http.Request, obj any) error {
|
||||
|
||||
if err := mapHeader(obj, req.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
||||
@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
|
||||
}
|
||||
|
||||
func decodeJSON(r io.Reader, obj any) error {
|
||||
decoder := json.NewDecoder(r)
|
||||
decoder := json.API.NewDecoder(r)
|
||||
if EnableDecoderUseNumber {
|
||||
decoder.UseNumber()
|
||||
}
|
||||
|
||||
@ -5,8 +5,16 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
|
||||
assert.Equal(t, "FOO", s["foo"])
|
||||
assert.Equal(t, "world", s["hello"])
|
||||
}
|
||||
|
||||
func TestCustomJsonCodec(t *testing.T) {
|
||||
// Restore json encoding configuration after testing
|
||||
oldMarshal := json.API
|
||||
defer func() {
|
||||
json.API = oldMarshal
|
||||
}()
|
||||
// Custom json api
|
||||
json.API = customJsonApi{}
|
||||
|
||||
// test decode json
|
||||
obj := customReq{}
|
||||
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, zeroTime, obj.TimeEmpty)
|
||||
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
|
||||
assert.Nil(t, obj.TimeNil)
|
||||
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
|
||||
// test encode json
|
||||
w := httptest.NewRecorder()
|
||||
err2 := (render.PureJSON{Data: obj}).Render(w)
|
||||
require.NoError(t, err2)
|
||||
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type customReq struct {
|
||||
TimeEmpty time.Time `json:"time_empty"`
|
||||
TimeStruct time.Time `json:"time_struct"`
|
||||
TimeNil *time.Time `json:"time_nil"`
|
||||
TimePointer *time.Time `json:"time_pointer"`
|
||||
}
|
||||
|
||||
var customConfig = jsoniter.Config{
|
||||
EscapeHTML: true,
|
||||
SortMapKeys: true,
|
||||
ValidateJsonRawMessage: true,
|
||||
}.Froze()
|
||||
|
||||
func init() {
|
||||
customConfig.RegisterExtension(&TimeEx{})
|
||||
customConfig.RegisterExtension(&TimePointerEx{})
|
||||
}
|
||||
|
||||
type customJsonApi struct{}
|
||||
|
||||
func (j customJsonApi) Marshal(v any) ([]byte, error) {
|
||||
return customConfig.Marshal(v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) Unmarshal(data []byte, v any) error {
|
||||
return customConfig.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return customConfig.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
|
||||
return customConfig.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
|
||||
return customConfig.NewDecoder(reader)
|
||||
}
|
||||
|
||||
// region Time Extension
|
||||
|
||||
var (
|
||||
zeroTime = time.Time{}
|
||||
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
|
||||
defaultTimeCodec = &timeCodec{}
|
||||
)
|
||||
|
||||
type TimeEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timeType {
|
||||
return defaultTimeCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timeCodec struct{}
|
||||
|
||||
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((*time.Time)(ptr))
|
||||
return t.Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((*time.Time)(ptr))
|
||||
if t.Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((*time.Time)(ptr)) = zeroTime
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((*time.Time)(ptr)) = t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region *Time Extension
|
||||
|
||||
var (
|
||||
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
|
||||
defaultTimePointerCodec = &timePointerCodec{}
|
||||
)
|
||||
|
||||
type TimePointerEx struct {
|
||||
jsoniter.DummyExtension
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
|
||||
if typ == timePointerType {
|
||||
return defaultTimePointerCodec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type timePointerCodec struct{}
|
||||
|
||||
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
|
||||
t := *((**time.Time)(ptr))
|
||||
return t == nil || (*t).Equal(zeroTime)
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((**time.Time)(ptr))
|
||||
if t == nil || (*t).Equal(zeroTime) {
|
||||
stream.WriteNil()
|
||||
return
|
||||
}
|
||||
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
ts := iter.ReadString()
|
||||
if len(ts) == 0 {
|
||||
*((**time.Time)(ptr)) = nil
|
||||
return
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*((**time.Time)(ptr)) = &t
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFormMultipartBindingBindOneFile(t *testing.T) {
|
||||
@ -27,7 +28,7 @@ func TestFormMultipartBindingBindOneFile(t *testing.T) {
|
||||
|
||||
req := createRequestMultipartFiles(t, file)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertMultipartFileHeader(t, &s.FileValue, file)
|
||||
assertMultipartFileHeader(t, s.FilePtr, file)
|
||||
@ -53,7 +54,7 @@ func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
|
||||
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, s.SliceValues, len(files))
|
||||
assert.Len(t, s.SlicePtrs, len(files))
|
||||
@ -90,7 +91,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
|
||||
} {
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, tt.s)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,17 +107,17 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
|
||||
mw := multipart.NewWriter(&body)
|
||||
for _, file := range files {
|
||||
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
n, err := fw.Write(file.Content)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(file.Content), n)
|
||||
}
|
||||
err := mw.Close()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", "/", &body)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, "/", &body)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
|
||||
return req
|
||||
@ -127,12 +128,12 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test
|
||||
assert.Equal(t, int64(len(file.Content)), fh.Size)
|
||||
|
||||
fl, err := fh.Open()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(fl)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(file.Content), string(body))
|
||||
|
||||
err = fl.Close()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
56
binding/plain.go
Normal file
56
binding/plain.go
Normal file
@ -0,0 +1,56 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
|
||||
type plainBinding struct{}
|
||||
|
||||
func (plainBinding) Name() string {
|
||||
return "plain"
|
||||
}
|
||||
|
||||
func (plainBinding) Bind(req *http.Request, obj any) error {
|
||||
all, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decodePlain(all, obj)
|
||||
}
|
||||
|
||||
func (plainBinding) BindBody(body []byte, obj any) error {
|
||||
return decodePlain(body, obj)
|
||||
}
|
||||
|
||||
func decodePlain(data []byte, obj any) error {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(obj)
|
||||
|
||||
for v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.String {
|
||||
v.SetString(bytesconv.BytesToString(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := v.Interface().([]byte); ok {
|
||||
v.SetBytes(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("type (%T) unknown type", v)
|
||||
}
|
||||
@ -34,7 +34,7 @@ func (protobufBinding) BindBody(body []byte, obj any) error {
|
||||
if err := proto.Unmarshal(body, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
// Here it's same to return validate(obj), but util now we can't add
|
||||
// Here it's same to return validate(obj), but until now we can't add
|
||||
// `binding:""` to the struct which automatically generate by gen-proto
|
||||
return nil
|
||||
// return validate(obj)
|
||||
|
||||
@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(obj)
|
||||
return validate(obj)
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testInterface interface {
|
||||
@ -113,10 +114,10 @@ func TestValidateNoValidationValues(t *testing.T) {
|
||||
test := createNoValidationValues()
|
||||
empty := structNoValidationValues{}
|
||||
|
||||
assert.Nil(t, validate(test))
|
||||
assert.Nil(t, validate(&test))
|
||||
assert.Nil(t, validate(empty))
|
||||
assert.Nil(t, validate(&empty))
|
||||
require.NoError(t, validate(test))
|
||||
require.NoError(t, validate(&test))
|
||||
require.NoError(t, validate(empty))
|
||||
require.NoError(t, validate(&empty))
|
||||
|
||||
assert.Equal(t, origin, test)
|
||||
}
|
||||
@ -157,38 +158,38 @@ type structNoValidationPointer struct {
|
||||
}
|
||||
|
||||
func TestValidateNoValidationPointers(t *testing.T) {
|
||||
//origin := createNoValidation_values()
|
||||
//test := createNoValidation_values()
|
||||
// origin := createNoValidation_values()
|
||||
// test := createNoValidation_values()
|
||||
empty := structNoValidationPointer{}
|
||||
|
||||
//assert.Nil(t, validate(test))
|
||||
//assert.Nil(t, validate(&test))
|
||||
assert.Nil(t, validate(empty))
|
||||
assert.Nil(t, validate(&empty))
|
||||
// assert.Nil(t, validate(test))
|
||||
// assert.Nil(t, validate(&test))
|
||||
require.NoError(t, validate(empty))
|
||||
require.NoError(t, validate(&empty))
|
||||
|
||||
//assert.Equal(t, origin, test)
|
||||
// assert.Equal(t, origin, test)
|
||||
}
|
||||
|
||||
type Object map[string]any
|
||||
|
||||
func TestValidatePrimitives(t *testing.T) {
|
||||
obj := Object{"foo": "bar", "bar": 1}
|
||||
assert.NoError(t, validate(obj))
|
||||
assert.NoError(t, validate(&obj))
|
||||
require.NoError(t, validate(obj))
|
||||
require.NoError(t, validate(&obj))
|
||||
assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
|
||||
|
||||
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
|
||||
assert.NoError(t, validate(obj2))
|
||||
assert.NoError(t, validate(&obj2))
|
||||
require.NoError(t, validate(obj2))
|
||||
require.NoError(t, validate(&obj2))
|
||||
|
||||
nu := 10
|
||||
assert.NoError(t, validate(nu))
|
||||
assert.NoError(t, validate(&nu))
|
||||
require.NoError(t, validate(nu))
|
||||
require.NoError(t, validate(&nu))
|
||||
assert.Equal(t, 10, nu)
|
||||
|
||||
str := "value"
|
||||
assert.NoError(t, validate(str))
|
||||
assert.NoError(t, validate(&str))
|
||||
require.NoError(t, validate(str))
|
||||
require.NoError(t, validate(&str))
|
||||
assert.Equal(t, "value", str)
|
||||
}
|
||||
|
||||
@ -197,7 +198,7 @@ type structModifyValidation struct {
|
||||
}
|
||||
|
||||
func toZero(sl validator.StructLevel) {
|
||||
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
|
||||
s := sl.Top().Interface().(*structModifyValidation)
|
||||
s.Integer = 0
|
||||
}
|
||||
|
||||
@ -212,8 +213,8 @@ func TestValidateAndModifyStruct(t *testing.T) {
|
||||
s := structModifyValidation{Integer: 1}
|
||||
errs := validate(&s)
|
||||
|
||||
assert.Nil(t, errs)
|
||||
assert.Equal(t, s, structModifyValidation{Integer: 0})
|
||||
require.NoError(t, errs)
|
||||
assert.Equal(t, structModifyValidation{Integer: 0}, s)
|
||||
}
|
||||
|
||||
// structCustomValidation is a helper struct we use to check that
|
||||
@ -239,14 +240,14 @@ func TestValidatorEngine(t *testing.T) {
|
||||
|
||||
err := engine.RegisterValidation("notone", notOne)
|
||||
// Check that we can register custom validation without error
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an instance which will fail validation
|
||||
withOne := structCustomValidation{Integer: 1}
|
||||
errs := validate(withOne)
|
||||
|
||||
// Check that we got back non-nil errs
|
||||
assert.NotNil(t, errs)
|
||||
require.Error(t, errs)
|
||||
// Check that the error matches expectation
|
||||
assert.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 {
|
||||
return decodeXML(bytes.NewReader(body), obj)
|
||||
}
|
||||
|
||||
func decodeXML(r io.Reader, obj any) error {
|
||||
decoder := xml.NewDecoder(r)
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type yamlBinding struct{}
|
||||
|
||||
57
codec/json/api.go
Normal file
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)
|
||||
}
|
||||
460
context.go
460
context.go
@ -6,8 +6,11 @@ package gin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"maps"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
@ -34,7 +37,10 @@ const (
|
||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||
MIMEBSON = binding.MIMEBSON
|
||||
)
|
||||
|
||||
// BodyBytesKey indicates a default body bytes key.
|
||||
@ -70,7 +76,7 @@ type Context struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Keys is a key/value pair exclusively for the context of each request.
|
||||
Keys map[string]any
|
||||
Keys map[any]any
|
||||
|
||||
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
|
||||
Errors errorMsgs
|
||||
@ -127,11 +133,8 @@ func (c *Context) Copy() *Context {
|
||||
cp.fullPath = c.fullPath
|
||||
|
||||
cKeys := c.Keys
|
||||
cp.Keys = make(map[string]any, len(cKeys))
|
||||
c.mu.RLock()
|
||||
for k, v := range cKeys {
|
||||
cp.Keys[k] = v
|
||||
}
|
||||
cp.Keys = maps.Clone(cKeys)
|
||||
c.mu.RUnlock()
|
||||
|
||||
cParams := c.Params
|
||||
@ -152,6 +155,9 @@ func (c *Context) HandlerName() string {
|
||||
func (c *Context) HandlerNames() []string {
|
||||
hn := make([]string, 0, len(c.handlers))
|
||||
for _, val := range c.handlers {
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
hn = append(hn, nameOfFunction(val))
|
||||
}
|
||||
return hn
|
||||
@ -181,8 +187,10 @@ func (c *Context) FullPath() string {
|
||||
// See example in GitHub.
|
||||
func (c *Context) Next() {
|
||||
c.index++
|
||||
for c.index < int8(len(c.handlers)) {
|
||||
c.handlers[c.index](c)
|
||||
for c.index < safeInt8(len(c.handlers)) {
|
||||
if c.handlers[c.index] != nil {
|
||||
c.handlers[c.index](c)
|
||||
}
|
||||
c.index++
|
||||
}
|
||||
}
|
||||
@ -208,6 +216,14 @@ func (c *Context) AbortWithStatus(code int) {
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
|
||||
// This method stops the chain, writes the status code and return a JSON body without escaping.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
|
||||
c.Abort()
|
||||
c.PureJSON(code, jsonObj)
|
||||
}
|
||||
|
||||
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
|
||||
// This method stops the chain, writes the status code and return a JSON body.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
@ -256,12 +272,12 @@ func (c *Context) Error(err error) *Error {
|
||||
/************************************/
|
||||
|
||||
// Set is used to store a new key/value pair exclusively for this context.
|
||||
// It also lazy initializes c.Keys if it was not used previously.
|
||||
func (c *Context) Set(key string, value any) {
|
||||
// It also lazy initializes c.Keys if it was not used previously.
|
||||
func (c *Context) Set(key any, value any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys == nil {
|
||||
c.Keys = make(map[string]any)
|
||||
c.Keys = make(map[any]any)
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
@ -269,7 +285,7 @@ func (c *Context) Set(key string, value any) {
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
// If the value does not exist it returns (nil, false)
|
||||
func (c *Context) Get(key string) (value any, exists bool) {
|
||||
func (c *Context) Get(key any) (value any, exists bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, exists = c.Keys[key]
|
||||
@ -277,115 +293,198 @@ func (c *Context) Get(key string) (value any, exists bool) {
|
||||
}
|
||||
|
||||
// MustGet returns the value for the given key if it exists, otherwise it panics.
|
||||
func (c *Context) MustGet(key string) any {
|
||||
func (c *Context) MustGet(key any) any {
|
||||
if value, exists := c.Get(key); exists {
|
||||
return value
|
||||
}
|
||||
panic("Key \"" + key + "\" does not exist")
|
||||
panic(fmt.Sprintf("key %v does not exist", key))
|
||||
}
|
||||
|
||||
func getTyped[T any](c *Context, key any) (res T) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
res, _ = val.(T)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetString returns the value associated with the key as a string.
|
||||
func (c *Context) GetString(key string) (s string) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
s, _ = val.(string)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetString(key any) string {
|
||||
return getTyped[string](c, key)
|
||||
}
|
||||
|
||||
// GetBool returns the value associated with the key as a boolean.
|
||||
func (c *Context) GetBool(key string) (b bool) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
b, _ = val.(bool)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetBool(key any) bool {
|
||||
return getTyped[bool](c, key)
|
||||
}
|
||||
|
||||
// GetInt returns the value associated with the key as an integer.
|
||||
func (c *Context) GetInt(key string) (i int) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
i, _ = val.(int)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetInt(key any) int {
|
||||
return getTyped[int](c, key)
|
||||
}
|
||||
|
||||
// GetInt64 returns the value associated with the key as an integer.
|
||||
func (c *Context) GetInt64(key string) (i64 int64) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
i64, _ = val.(int64)
|
||||
}
|
||||
return
|
||||
// GetInt8 returns the value associated with the key as an integer 8.
|
||||
func (c *Context) GetInt8(key any) int8 {
|
||||
return getTyped[int8](c, key)
|
||||
}
|
||||
|
||||
// GetInt16 returns the value associated with the key as an integer 16.
|
||||
func (c *Context) GetInt16(key any) int16 {
|
||||
return getTyped[int16](c, key)
|
||||
}
|
||||
|
||||
// GetInt32 returns the value associated with the key as an integer 32.
|
||||
func (c *Context) GetInt32(key any) int32 {
|
||||
return getTyped[int32](c, key)
|
||||
}
|
||||
|
||||
// GetInt64 returns the value associated with the key as an integer 64.
|
||||
func (c *Context) GetInt64(key any) int64 {
|
||||
return getTyped[int64](c, key)
|
||||
}
|
||||
|
||||
// GetUint returns the value associated with the key as an unsigned integer.
|
||||
func (c *Context) GetUint(key string) (ui uint) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
ui, _ = val.(uint)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetUint(key any) uint {
|
||||
return getTyped[uint](c, key)
|
||||
}
|
||||
|
||||
// GetUint64 returns the value associated with the key as an unsigned integer.
|
||||
func (c *Context) GetUint64(key string) (ui64 uint64) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
ui64, _ = val.(uint64)
|
||||
}
|
||||
return
|
||||
// GetUint8 returns the value associated with the key as an unsigned integer 8.
|
||||
func (c *Context) GetUint8(key any) uint8 {
|
||||
return getTyped[uint8](c, key)
|
||||
}
|
||||
|
||||
// GetUint16 returns the value associated with the key as an unsigned integer 16.
|
||||
func (c *Context) GetUint16(key any) uint16 {
|
||||
return getTyped[uint16](c, key)
|
||||
}
|
||||
|
||||
// GetUint32 returns the value associated with the key as an unsigned integer 32.
|
||||
func (c *Context) GetUint32(key any) uint32 {
|
||||
return getTyped[uint32](c, key)
|
||||
}
|
||||
|
||||
// GetUint64 returns the value associated with the key as an unsigned integer 64.
|
||||
func (c *Context) GetUint64(key any) uint64 {
|
||||
return getTyped[uint64](c, key)
|
||||
}
|
||||
|
||||
// GetFloat32 returns the value associated with the key as a float32.
|
||||
func (c *Context) GetFloat32(key any) float32 {
|
||||
return getTyped[float32](c, key)
|
||||
}
|
||||
|
||||
// GetFloat64 returns the value associated with the key as a float64.
|
||||
func (c *Context) GetFloat64(key string) (f64 float64) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
f64, _ = val.(float64)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetFloat64(key any) float64 {
|
||||
return getTyped[float64](c, key)
|
||||
}
|
||||
|
||||
// GetTime returns the value associated with the key as time.
|
||||
func (c *Context) GetTime(key string) (t time.Time) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
t, _ = val.(time.Time)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetTime(key any) time.Time {
|
||||
return getTyped[time.Time](c, key)
|
||||
}
|
||||
|
||||
// GetDuration returns the value associated with the key as a duration.
|
||||
func (c *Context) GetDuration(key string) (d time.Duration) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
d, _ = val.(time.Duration)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetDuration(key any) time.Duration {
|
||||
return getTyped[time.Duration](c, key)
|
||||
}
|
||||
|
||||
// GetError returns the value associated with the key as an error.
|
||||
func (c *Context) GetError(key any) error {
|
||||
return getTyped[error](c, key)
|
||||
}
|
||||
|
||||
// GetIntSlice returns the value associated with the key as a slice of integers.
|
||||
func (c *Context) GetIntSlice(key any) []int {
|
||||
return getTyped[[]int](c, key)
|
||||
}
|
||||
|
||||
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
|
||||
func (c *Context) GetInt8Slice(key any) []int8 {
|
||||
return getTyped[[]int8](c, key)
|
||||
}
|
||||
|
||||
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
|
||||
func (c *Context) GetInt16Slice(key any) []int16 {
|
||||
return getTyped[[]int16](c, key)
|
||||
}
|
||||
|
||||
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
|
||||
func (c *Context) GetInt32Slice(key any) []int32 {
|
||||
return getTyped[[]int32](c, key)
|
||||
}
|
||||
|
||||
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
|
||||
func (c *Context) GetInt64Slice(key any) []int64 {
|
||||
return getTyped[[]int64](c, key)
|
||||
}
|
||||
|
||||
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
|
||||
func (c *Context) GetUintSlice(key any) []uint {
|
||||
return getTyped[[]uint](c, key)
|
||||
}
|
||||
|
||||
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
|
||||
func (c *Context) GetUint8Slice(key any) []uint8 {
|
||||
return getTyped[[]uint8](c, key)
|
||||
}
|
||||
|
||||
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
|
||||
func (c *Context) GetUint16Slice(key any) []uint16 {
|
||||
return getTyped[[]uint16](c, key)
|
||||
}
|
||||
|
||||
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
|
||||
func (c *Context) GetUint32Slice(key any) []uint32 {
|
||||
return getTyped[[]uint32](c, key)
|
||||
}
|
||||
|
||||
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
|
||||
func (c *Context) GetUint64Slice(key any) []uint64 {
|
||||
return getTyped[[]uint64](c, key)
|
||||
}
|
||||
|
||||
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
|
||||
func (c *Context) GetFloat32Slice(key any) []float32 {
|
||||
return getTyped[[]float32](c, key)
|
||||
}
|
||||
|
||||
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
|
||||
func (c *Context) GetFloat64Slice(key any) []float64 {
|
||||
return getTyped[[]float64](c, key)
|
||||
}
|
||||
|
||||
// GetStringSlice returns the value associated with the key as a slice of strings.
|
||||
func (c *Context) GetStringSlice(key string) (ss []string) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
ss, _ = val.([]string)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetStringSlice(key any) []string {
|
||||
return getTyped[[]string](c, key)
|
||||
}
|
||||
|
||||
// GetErrorSlice returns the value associated with the key as a slice of errors.
|
||||
func (c *Context) GetErrorSlice(key any) []error {
|
||||
return getTyped[[]error](c, key)
|
||||
}
|
||||
|
||||
// GetStringMap returns the value associated with the key as a map of interfaces.
|
||||
func (c *Context) GetStringMap(key string) (sm map[string]any) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
sm, _ = val.(map[string]any)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetStringMap(key any) map[string]any {
|
||||
return getTyped[map[string]any](c, key)
|
||||
}
|
||||
|
||||
// GetStringMapString returns the value associated with the key as a map of strings.
|
||||
func (c *Context) GetStringMapString(key string) (sms map[string]string) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
sms, _ = val.(map[string]string)
|
||||
}
|
||||
return
|
||||
func (c *Context) GetStringMapString(key any) map[string]string {
|
||||
return getTyped[map[string]string](c, key)
|
||||
}
|
||||
|
||||
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
|
||||
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
|
||||
if val, ok := c.Get(key); ok && val != nil {
|
||||
smss, _ = val.(map[string][]string)
|
||||
func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
|
||||
return getTyped[map[string][]string](c, key)
|
||||
}
|
||||
|
||||
// Delete deletes the key from the Context's Key map, if it exists.
|
||||
// This operation is safe to be used by concurrent go-routines
|
||||
func (c *Context) Delete(key any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.Keys != nil {
|
||||
delete(c.Keys, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/************************************/
|
||||
@ -468,7 +567,7 @@ func (c *Context) QueryArray(key string) (values []string) {
|
||||
|
||||
func (c *Context) initQueryCache() {
|
||||
if c.queryCache == nil {
|
||||
if c.Request != nil {
|
||||
if c.Request != nil && c.Request.URL != nil {
|
||||
c.queryCache = c.Request.URL.Query()
|
||||
} else {
|
||||
c.queryCache = url.Values{}
|
||||
@ -494,7 +593,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
|
||||
// whether at least one value exists for the given key.
|
||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
||||
c.initQueryCache()
|
||||
return c.get(c.queryCache, key)
|
||||
return getMapFromFormData(c.queryCache, key)
|
||||
}
|
||||
|
||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||
@ -567,22 +666,32 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
|
||||
// whether at least one value exists for the given key.
|
||||
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
||||
c.initFormCache()
|
||||
return c.get(c.formCache, key)
|
||||
return getMapFromFormData(c.formCache, key)
|
||||
}
|
||||
|
||||
// get is an internal method and returns a map which satisfies conditions.
|
||||
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
|
||||
dicts := make(map[string]string)
|
||||
exist := false
|
||||
// getMapFromFormData return a map which satisfies conditions.
|
||||
// It parses from data with bracket notation like "key[subkey]=value" into a map.
|
||||
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
|
||||
d := make(map[string]string)
|
||||
found := false
|
||||
keyLen := len(key)
|
||||
|
||||
for k, v := range m {
|
||||
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
|
||||
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
|
||||
exist = true
|
||||
dicts[k[i+1:][:j]] = v[0]
|
||||
}
|
||||
if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
|
||||
continue
|
||||
}
|
||||
|
||||
if k[:keyLen] != key || k[keyLen] != '[' {
|
||||
continue
|
||||
}
|
||||
|
||||
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
|
||||
found = true
|
||||
d[k[keyLen+1:keyLen+1+j]] = v[0]
|
||||
}
|
||||
}
|
||||
return dicts, exist
|
||||
|
||||
return d, found
|
||||
}
|
||||
|
||||
// FormFile returns the first file for the provided form key.
|
||||
@ -607,14 +716,22 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
|
||||
}
|
||||
|
||||
// SaveUploadedFile uploads the form file to specific dst.
|
||||
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
|
||||
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
||||
var mode os.FileMode = 0o750
|
||||
if len(perm) > 0 {
|
||||
mode = perm[0]
|
||||
}
|
||||
dir := filepath.Dir(dst)
|
||||
if err = os.MkdirAll(dir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Chmod(dir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -634,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
//
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||
// It decodes the payload into the struct specified as a pointer.
|
||||
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
||||
func (c *Context) Bind(obj any) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
@ -667,6 +784,11 @@ func (c *Context) BindTOML(obj any) error {
|
||||
return c.MustBindWith(obj, binding.TOML)
|
||||
}
|
||||
|
||||
// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
|
||||
func (c *Context) BindPlain(obj any) error {
|
||||
return c.MustBindWith(obj, binding.Plain)
|
||||
}
|
||||
|
||||
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
|
||||
func (c *Context) BindHeader(obj any) error {
|
||||
return c.MustBindWith(obj, binding.Header)
|
||||
@ -686,8 +808,19 @@ func (c *Context) BindUri(obj any) error {
|
||||
// It will abort the request with HTTP 400 if any error occurs.
|
||||
// See the binding package.
|
||||
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
||||
if err := c.ShouldBindWith(obj, b); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
err := c.ShouldBindWith(obj, b)
|
||||
if err != nil {
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
|
||||
// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
|
||||
// https://github.com/goccy/go-json/issues/485
|
||||
// https://github.com/bytedance/sonic/issues/800
|
||||
switch {
|
||||
case errors.As(err, &maxBytesErr):
|
||||
c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
default:
|
||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -699,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
//
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||
// It decodes the payload into the struct specified as a pointer.
|
||||
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
|
||||
func (c *Context) ShouldBind(obj any) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
@ -708,36 +841,71 @@ func (c *Context) ShouldBind(obj any) error {
|
||||
}
|
||||
|
||||
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// POST /user
|
||||
// Content-Type: application/json
|
||||
//
|
||||
// Request Body:
|
||||
// {
|
||||
// "name": "Manu",
|
||||
// "age": 20
|
||||
// }
|
||||
//
|
||||
// type User struct {
|
||||
// Name string `json:"name"`
|
||||
// Age int `json:"age"`
|
||||
// }
|
||||
//
|
||||
// var user User
|
||||
// if err := c.ShouldBindJSON(&user); err != nil {
|
||||
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
// return
|
||||
// }
|
||||
// c.JSON(http.StatusOK, user)
|
||||
func (c *Context) ShouldBindJSON(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.JSON)
|
||||
}
|
||||
|
||||
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
|
||||
// It works like ShouldBindJSON but binds the request body as XML data.
|
||||
func (c *Context) ShouldBindXML(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.XML)
|
||||
}
|
||||
|
||||
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
|
||||
// It works like ShouldBindJSON but binds query parameters from the URL.
|
||||
func (c *Context) ShouldBindQuery(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.Query)
|
||||
}
|
||||
|
||||
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
|
||||
// It works like ShouldBindJSON but binds the request body as YAML data.
|
||||
func (c *Context) ShouldBindYAML(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
|
||||
// It works like ShouldBindJSON but binds the request body as TOML data.
|
||||
func (c *Context) ShouldBindTOML(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.TOML)
|
||||
}
|
||||
|
||||
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
|
||||
// It works like ShouldBindJSON but binds plain text data from the request body.
|
||||
func (c *Context) ShouldBindPlain(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.Plain)
|
||||
}
|
||||
|
||||
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
||||
// It works like ShouldBindJSON but binds values from HTTP headers.
|
||||
func (c *Context) ShouldBindHeader(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.Header)
|
||||
}
|
||||
|
||||
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||
// It works like ShouldBindJSON but binds parameters from the URI.
|
||||
func (c *Context) ShouldBindUri(obj any) error {
|
||||
m := make(map[string][]string, len(c.Params))
|
||||
for _, v := range c.Params {
|
||||
@ -794,9 +962,14 @@ func (c *Context) ShouldBindBodyWithTOML(obj any) error {
|
||||
return c.ShouldBindBodyWith(obj, binding.TOML)
|
||||
}
|
||||
|
||||
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
|
||||
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
|
||||
return c.ShouldBindBodyWith(obj, binding.Plain)
|
||||
}
|
||||
|
||||
// ClientIP implements one best effort algorithm to return the real client IP.
|
||||
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
|
||||
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
|
||||
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]).
|
||||
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
|
||||
// the remote IP (coming from Request.RemoteAddr) is returned.
|
||||
func (c *Context) ClientIP() string {
|
||||
@ -816,18 +989,32 @@ func (c *Context) ClientIP() string {
|
||||
}
|
||||
}
|
||||
|
||||
// It also checks if the remoteIP is a trusted proxy or not.
|
||||
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
||||
// defined by Engine.SetTrustedProxies()
|
||||
remoteIP := net.ParseIP(c.RemoteIP())
|
||||
if remoteIP == nil {
|
||||
return ""
|
||||
var (
|
||||
trusted bool
|
||||
remoteIP net.IP
|
||||
)
|
||||
// If gin is listening a unix socket, always trust it.
|
||||
localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
|
||||
trusted = true
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if !trusted {
|
||||
// It also checks if the remoteIP is a trusted proxy or not.
|
||||
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
|
||||
// defined by Engine.SetTrustedProxies()
|
||||
remoteIP = net.ParseIP(c.RemoteIP())
|
||||
if remoteIP == nil {
|
||||
return ""
|
||||
}
|
||||
trusted = c.engine.isTrustedProxy(remoteIP)
|
||||
}
|
||||
trusted := c.engine.isTrustedProxy(remoteIP)
|
||||
|
||||
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
|
||||
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
|
||||
ip, valid := c.engine.validateHeader(headerValue)
|
||||
if valid {
|
||||
return ip
|
||||
}
|
||||
@ -869,9 +1056,10 @@ func (c *Context) requestHeader(key string) string {
|
||||
/************************************/
|
||||
|
||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
||||
// Uses http.StatusContinue constant for better code clarity.
|
||||
func bodyAllowedForStatus(status int) bool {
|
||||
switch {
|
||||
case status >= 100 && status <= 199:
|
||||
case status >= http.StatusContinue && status < http.StatusOK:
|
||||
return false
|
||||
case status == http.StatusNoContent:
|
||||
return false
|
||||
@ -934,6 +1122,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
|
||||
})
|
||||
}
|
||||
|
||||
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
|
||||
func (c *Context) SetCookieData(cookie *http.Cookie) {
|
||||
if cookie.Path == "" {
|
||||
cookie.Path = "/"
|
||||
}
|
||||
if cookie.SameSite == http.SameSiteDefaultMode {
|
||||
cookie.SameSite = c.sameSite
|
||||
}
|
||||
http.SetCookie(c.Writer, cookie)
|
||||
}
|
||||
|
||||
// Cookie returns the named cookie provided in the request or
|
||||
// ErrNoCookie if not found. And return the named cookie is unescaped.
|
||||
// If multiple cookies match the given name, only one cookie will
|
||||
@ -1023,6 +1224,12 @@ func (c *Context) XML(code int, obj any) {
|
||||
c.Render(code, render.XML{Data: obj})
|
||||
}
|
||||
|
||||
// PDF writes the given PDF binary data into the response body.
|
||||
// It also sets the Content-Type as "application/pdf".
|
||||
func (c *Context) PDF(code int, data []byte) {
|
||||
c.Render(code, render.PDF{Data: data})
|
||||
}
|
||||
|
||||
// YAML serializes the given struct as YAML into the response body.
|
||||
func (c *Context) YAML(code int, obj any) {
|
||||
c.Render(code, render.YAML{Data: obj})
|
||||
@ -1038,6 +1245,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
|
||||
c.Render(code, render.ProtoBuf{Data: obj})
|
||||
}
|
||||
|
||||
// BSON serializes the given struct as BSON into the response body.
|
||||
func (c *Context) BSON(code int, obj any) {
|
||||
c.Render(code, render.BSON{Data: obj})
|
||||
}
|
||||
|
||||
// String writes the given string into the response body.
|
||||
func (c *Context) String(code int, format string, values ...any) {
|
||||
c.Render(code, render.String{Format: format, Data: values})
|
||||
@ -1136,14 +1348,16 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
|
||||
|
||||
// Negotiate contains all negotiations data.
|
||||
type Negotiate struct {
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
PROTOBUFData any
|
||||
BSONData any
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according to acceptable Accept format.
|
||||
@ -1161,7 +1375,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.XMLData, config.Data)
|
||||
c.XML(code, data)
|
||||
|
||||
case binding.MIMEYAML:
|
||||
case binding.MIMEYAML, binding.MIMEYAML2:
|
||||
data := chooseData(config.YAMLData, config.Data)
|
||||
c.YAML(code, data)
|
||||
|
||||
@ -1169,6 +1383,14 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.TOMLData, config.Data)
|
||||
c.TOML(code, data)
|
||||
|
||||
case binding.MIMEPROTOBUF:
|
||||
data := chooseData(config.PROTOBUFData, config.Data)
|
||||
c.ProtoBuf(code, data)
|
||||
|
||||
case binding.MIMEBSON:
|
||||
data := chooseData(config.BSONData, config.Data)
|
||||
c.BSON(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
// Copyright 2021 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 !go1.19
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContextFormFileFailed18(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
mw := multipart.NewWriter(buf)
|
||||
defer func(mw *multipart.Writer) {
|
||||
err := mw.Close()
|
||||
if err != nil {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}(mw)
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
c.engine.MaxMultipartMemory = 8 << 20
|
||||
assert.Panics(t, func() {
|
||||
f, err := c.FormFile("file")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, f)
|
||||
})
|
||||
}
|
||||
@ -1,30 +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 go1.19
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContextFormFileFailed19(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
mw := multipart.NewWriter(buf)
|
||||
mw.Close()
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
c.engine.MaxMultipartMemory = 8 << 20
|
||||
f, err := c.FormFile("file")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, f)
|
||||
}
|
||||
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)
|
||||
}
|
||||
1537
context_test.go
1537
context_test.go
File diff suppressed because it is too large
Load Diff
13
debug.go
13
debug.go
@ -10,21 +10,24 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const ginSupportMinGoVer = 18
|
||||
const ginSupportMinGoVer = 25
|
||||
|
||||
var runtimeVersion = runtime.Version()
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
func IsDebugging() bool {
|
||||
return ginMode == debugCode
|
||||
return atomic.LoadInt32(&ginMode) == debugCode
|
||||
}
|
||||
|
||||
// DebugPrintRouteFunc indicates debug log output format.
|
||||
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
|
||||
|
||||
// DebugPrintFunc indicates debug log output format.
|
||||
var DebugPrintFunc func(format string, values ...interface{})
|
||||
var DebugPrintFunc func(format string, values ...any)
|
||||
|
||||
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||
if IsDebugging() {
|
||||
@ -76,8 +79,8 @@ func getMinVer(v string) (uint64, error) {
|
||||
}
|
||||
|
||||
func debugPrintWARNINGDefault() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.18+.
|
||||
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.25+.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
@ -10,19 +10,16 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO
|
||||
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||
// func debugPrint(format string, values ...any) {
|
||||
|
||||
func TestIsDebugging(t *testing.T) {
|
||||
SetMode(DebugMode)
|
||||
assert.True(t, IsDebugging())
|
||||
@ -46,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
|
||||
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintFunc(t *testing.T) {
|
||||
DebugPrintFunc = func(format string, values ...any) {
|
||||
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
|
||||
}
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrint("debug print func test: %d", 123)
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
|
||||
}
|
||||
|
||||
func TestDebugPrintError(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
@ -59,7 +68,7 @@ func TestDebugPrintError(t *testing.T) {
|
||||
func TestDebugPrintRoutes(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||
@ -71,7 +80,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
|
||||
}
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||
@ -102,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||
debugPrintWARNINGDefault()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m < ginSupportMinGoVer {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.18+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
} else {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
|
||||
runtimeVersion = "go1.23.12"
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintWARNINGDefault()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.25+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||
@ -154,13 +168,13 @@ func TestGetMinVer(t *testing.T) {
|
||||
var m uint64
|
||||
var e error
|
||||
_, e = getMinVer("go1")
|
||||
assert.NotNil(t, e)
|
||||
require.Error(t, e)
|
||||
m, e = getMinVer("go1.1")
|
||||
assert.Equal(t, uint64(1), m)
|
||||
assert.Nil(t, e)
|
||||
require.NoError(t, e)
|
||||
m, e = getMinVer("go1.1.1")
|
||||
assert.Nil(t, e)
|
||||
require.NoError(t, e)
|
||||
assert.Equal(t, uint64(1), m)
|
||||
_, e = getMinVer("go1.1.1.1")
|
||||
assert.NotNil(t, e)
|
||||
require.Error(t, e)
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
|
||||
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
|
||||
|
||||
var obj struct {
|
||||
Foo string `form:"foo"`
|
||||
|
||||
16
doc.go
16
doc.go
@ -2,5 +2,21 @@
|
||||
Package gin implements a HTTP web framework called gin.
|
||||
|
||||
See https://gin-gonic.com/ for more information about gin.
|
||||
|
||||
Example:
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
r.Run() // listen and serve on 0.0.0.0:8080
|
||||
}
|
||||
*/
|
||||
package gin // import "github.com/gin-gonic/gin"
|
||||
|
||||
1303
docs/doc.md
1303
docs/doc.md
File diff suppressed because it is too large
Load Diff
10
errors.go
10
errors.go
@ -9,7 +9,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
)
|
||||
|
||||
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
|
||||
@ -26,8 +26,6 @@ const (
|
||||
ErrorTypePublic ErrorType = 1 << 1
|
||||
// ErrorTypeAny indicates any other error.
|
||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||
// ErrorTypeNu indicates any other error.
|
||||
ErrorTypeNu = 2
|
||||
)
|
||||
|
||||
// Error represents a error's specification.
|
||||
@ -77,7 +75,7 @@ func (msg *Error) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(msg.JSON())
|
||||
return json.API.Marshal(msg.JSON())
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
@ -91,7 +89,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
|
||||
func (msg *Error) Unwrap() error {
|
||||
func (msg Error) Unwrap() error {
|
||||
return msg.Err
|
||||
}
|
||||
|
||||
@ -157,7 +155,7 @@ func (a errorMsgs) JSON() any {
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.JSON())
|
||||
return json.API.Marshal(a.JSON())
|
||||
}
|
||||
|
||||
func (a errorMsgs) String() string {
|
||||
|
||||
@ -9,8 +9,9 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
@ -32,8 +33,8 @@ func TestError(t *testing.T) {
|
||||
"meta": "some data",
|
||||
}, err.JSON())
|
||||
|
||||
jsonBytes, _ := json.Marshal(err)
|
||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
jsonBytes, _ := json.API.Marshal(err)
|
||||
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
|
||||
err.SetMeta(H{ //nolint: errcheck
|
||||
"status": "200",
|
||||
@ -91,14 +92,14 @@ Error #03: third
|
||||
H{"error": "second", "meta": "some data"},
|
||||
H{"error": "third", "status": "400"},
|
||||
}, errs.JSON())
|
||||
jsonBytes, _ := json.Marshal(errs)
|
||||
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||
jsonBytes, _ := json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
|
||||
errs = errorMsgs{
|
||||
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||
}
|
||||
assert.Equal(t, H{"error": "first"}, errs.JSON())
|
||||
jsonBytes, _ = json.Marshal(errs)
|
||||
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||
jsonBytes, _ = json.API.Marshal(errs)
|
||||
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
|
||||
|
||||
errs = errorMsgs{}
|
||||
assert.Nil(t, errs.Last())
|
||||
@ -122,7 +123,18 @@ func TestErrorUnwrap(t *testing.T) {
|
||||
})
|
||||
|
||||
// check that 'errors.Is()' and 'errors.As()' behave as expected :
|
||||
assert.True(t, errors.Is(err, innerErr))
|
||||
require.ErrorIs(t, err, innerErr)
|
||||
var testErr TestErr
|
||||
assert.True(t, errors.As(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)
|
||||
}
|
||||
|
||||
53
fs.go
53
fs.go
@ -9,37 +9,42 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type onlyFilesFS struct {
|
||||
fs http.FileSystem
|
||||
// OnlyFilesFS implements an http.FileSystem without `Readdir` functionality.
|
||||
type OnlyFilesFS struct {
|
||||
FileSystem http.FileSystem
|
||||
}
|
||||
|
||||
type neuteredReaddirFile struct {
|
||||
http.File
|
||||
}
|
||||
|
||||
// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
|
||||
// in router.Static().
|
||||
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
|
||||
// a filesystem that prevents http.FileServer() to list the directory files.
|
||||
func Dir(root string, listDirectory bool) http.FileSystem {
|
||||
fs := http.Dir(root)
|
||||
if listDirectory {
|
||||
return fs
|
||||
}
|
||||
return &onlyFilesFS{fs}
|
||||
}
|
||||
|
||||
// Open conforms to http.Filesystem.
|
||||
func (fs onlyFilesFS) Open(name string) (http.File, error) {
|
||||
f, err := fs.fs.Open(name)
|
||||
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
|
||||
func (o OnlyFilesFS) Open(name string) (http.File, error) {
|
||||
f, err := o.FileSystem.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return neuteredReaddirFile{f}, nil
|
||||
|
||||
return neutralizedReaddirFile{f}, nil
|
||||
}
|
||||
|
||||
// Readdir overrides the http.File default implementation.
|
||||
func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
|
||||
// neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
|
||||
type neutralizedReaddirFile struct {
|
||||
http.File
|
||||
}
|
||||
|
||||
// Readdir overrides the http.File default implementation and always returns nil.
|
||||
func (n neutralizedReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
|
||||
// this disables directory listing
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Dir returns an http.FileSystem that can be used by http.FileServer().
|
||||
// It is used internally in router.Static().
|
||||
// if listDirectory == true, then it works the same as http.Dir(),
|
||||
// otherwise it returns a filesystem that prevents http.FileServer() to list the directory files.
|
||||
func Dir(root string, listDirectory bool) http.FileSystem {
|
||||
fs := http.Dir(root)
|
||||
|
||||
if listDirectory {
|
||||
return fs
|
||||
}
|
||||
|
||||
return &OnlyFilesFS{FileSystem: fs}
|
||||
}
|
||||
|
||||
72
fs_test.go
Normal file
72
fs_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package gin
|
||||
|
||||
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 TestOnlyFilesFS_Open(t *testing.T) {
|
||||
var testFile *os.File
|
||||
mockFS := &mockFileSystem{
|
||||
open: func(name string) (http.File, error) {
|
||||
return testFile, nil
|
||||
},
|
||||
}
|
||||
fs := &OnlyFilesFS{FileSystem: mockFS}
|
||||
|
||||
file, err := fs.Open("foo")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
|
||||
}
|
||||
|
||||
func TestOnlyFilesFS_Open_err(t *testing.T) {
|
||||
testError := errors.New("mock")
|
||||
mockFS := &mockFileSystem{
|
||||
open: func(_ string) (http.File, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}
|
||||
fs := &OnlyFilesFS{FileSystem: mockFS}
|
||||
|
||||
file, err := fs.Open("foo")
|
||||
|
||||
require.ErrorIs(t, err, testError)
|
||||
assert.Nil(t, file)
|
||||
}
|
||||
|
||||
func Test_neuteredReaddirFile_Readdir(t *testing.T) {
|
||||
n := neutralizedReaddirFile{}
|
||||
|
||||
res, err := n.Readdir(0)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func TestDir_listDirectory(t *testing.T) {
|
||||
testRoot := "foo"
|
||||
fs := Dir(testRoot, true)
|
||||
|
||||
assert.Equal(t, http.Dir(testRoot), fs)
|
||||
}
|
||||
|
||||
func TestDir(t *testing.T) {
|
||||
testRoot := "foo"
|
||||
fs := Dir(testRoot, false)
|
||||
|
||||
assert.Equal(t, &OnlyFilesFS{FileSystem: http.Dir(testRoot)}, fs)
|
||||
}
|
||||
171
gin.go
171
gin.go
@ -5,24 +5,29 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
filesystem "github.com/gin-gonic/gin/internal/fs"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
||||
const (
|
||||
defaultMultipartMemory = 32 << 20 // 32 MB
|
||||
escapedColon = "\\:"
|
||||
colon = ":"
|
||||
backslash = "\\"
|
||||
)
|
||||
|
||||
var (
|
||||
default404Body = []byte("404 page not found")
|
||||
@ -42,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
)
|
||||
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
@ -92,6 +92,10 @@ const (
|
||||
type Engine struct {
|
||||
RouterGroup
|
||||
|
||||
// routeTreesUpdated ensures that the initialization or update of the route trees
|
||||
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
|
||||
routeTreesUpdated sync.Once
|
||||
|
||||
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
|
||||
// handler for the path with (without) the trailing slash exists.
|
||||
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||
@ -131,10 +135,16 @@ type Engine struct {
|
||||
AppEngine bool
|
||||
|
||||
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
||||
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
|
||||
// Only use RawPath if you know what you are doing.
|
||||
UseRawPath bool
|
||||
|
||||
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
|
||||
// It overrides UseRawPath
|
||||
UseEscapedPath bool
|
||||
|
||||
// UnescapePathValues if true, the path value will be unescaped.
|
||||
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
||||
// If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
|
||||
// as url.Path gonna be used, which is already unescaped.
|
||||
UnescapePathValues bool
|
||||
|
||||
@ -187,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
|
||||
// - HandleMethodNotAllowed: false
|
||||
// - ForwardedByClientIP: true
|
||||
// - UseRawPath: false
|
||||
// - UseEscapedPath: false
|
||||
// - UnescapePathValues: true
|
||||
func New(opts ...OptionFunc) *Engine {
|
||||
debugPrintWARNINGNew()
|
||||
@ -204,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||
TrustedPlatform: defaultPlatform,
|
||||
UseRawPath: false,
|
||||
UseEscapedPath: false,
|
||||
RemoveExtraSlash: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
@ -213,7 +225,7 @@ func New(opts ...OptionFunc) *Engine {
|
||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||
trustedCIDRs: defaultTrustedCIDRs,
|
||||
}
|
||||
engine.RouterGroup.engine = engine
|
||||
engine.engine = engine
|
||||
engine.pool.New = func() any {
|
||||
return engine.allocateContext(engine.maxParams)
|
||||
}
|
||||
@ -283,6 +295,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
|
||||
// and associates the result with HTML renderer.
|
||||
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
|
||||
if IsDebugging() {
|
||||
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
|
||||
return
|
||||
}
|
||||
|
||||
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
|
||||
filesystem.FileSystem{FileSystem: fs}, patterns...))
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate associate a template with HTML renderer.
|
||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||
if len(engine.trees) > 0 {
|
||||
@ -319,7 +344,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||
return engine
|
||||
}
|
||||
|
||||
// With returns a new Engine instance with the provided options.
|
||||
// With returns an Engine with the configuration set in the OptionFunc.
|
||||
func (engine *Engine) With(opts ...OptionFunc) *Engine {
|
||||
for _, opt := range opts {
|
||||
opt(engine)
|
||||
@ -361,7 +386,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
||||
}
|
||||
|
||||
// Routes returns a slice of registered routes, including some useful information, such as:
|
||||
// the http method, path and the handler name.
|
||||
// the http method, path, and the handler name.
|
||||
func (engine *Engine) Routes() (routes RoutesInfo) {
|
||||
for _, tree := range engine.trees {
|
||||
routes = iterate("", tree.method, routes, tree.root)
|
||||
@ -386,23 +411,6 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
|
||||
return routes
|
||||
}
|
||||
|
||||
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func (engine *Engine) Run(addr ...string) (err error) {
|
||||
defer func() { debugPrintError(err) }()
|
||||
|
||||
if engine.isUnsafeTrustedProxies() {
|
||||
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
|
||||
}
|
||||
|
||||
address := resolveAddress(addr)
|
||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
err = http.ListenAndServe(address, engine.Handler())
|
||||
return
|
||||
}
|
||||
|
||||
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
|
||||
if engine.trustedProxies == nil {
|
||||
return nil, nil
|
||||
@ -492,6 +500,26 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
||||
return "", false
|
||||
}
|
||||
|
||||
// updateRouteTree do update to the route tree recursively
|
||||
func updateRouteTree(n *node) {
|
||||
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
||||
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
|
||||
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
|
||||
if n.children == nil {
|
||||
return
|
||||
}
|
||||
for _, child := range n.children {
|
||||
updateRouteTree(child)
|
||||
}
|
||||
}
|
||||
|
||||
// updateRouteTrees do update to the route trees
|
||||
func (engine *Engine) updateRouteTrees() {
|
||||
for _, tree := range engine.trees {
|
||||
updateRouteTree(tree.root)
|
||||
}
|
||||
}
|
||||
|
||||
// parseIP parse a string representation of an IP and returns a net.IP with the
|
||||
// minimum byte representation or nil if input is invalid.
|
||||
func parseIP(ip string) net.IP {
|
||||
@ -506,6 +534,27 @@ func parseIP(ip string) net.IP {
|
||||
return parsedIP
|
||||
}
|
||||
|
||||
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
|
||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func (engine *Engine) Run(addr ...string) (err error) {
|
||||
defer func() { debugPrintError(err) }()
|
||||
|
||||
if engine.isUnsafeTrustedProxies() {
|
||||
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||
}
|
||||
engine.updateRouteTrees()
|
||||
address := resolveAddress(addr)
|
||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||
server := &http.Server{ // #nosec G112
|
||||
Addr: address,
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.ListenAndServe()
|
||||
return
|
||||
}
|
||||
|
||||
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
|
||||
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
@ -515,17 +564,13 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
|
||||
if engine.isUnsafeTrustedProxies() {
|
||||
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
|
||||
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
server := &http.Server{ // #nosec G112
|
||||
Addr: addr,
|
||||
Handler: engine.Handler(),
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // TLS 1.2 or higher
|
||||
},
|
||||
}
|
||||
|
||||
err = server.ListenAndServeTLS(certFile, keyFile)
|
||||
return
|
||||
}
|
||||
@ -549,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
defer listener.Close()
|
||||
defer os.Remove(file)
|
||||
|
||||
err = http.Serve(listener, engine.Handler())
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
return
|
||||
}
|
||||
|
||||
@ -566,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||
defer f.Close()
|
||||
listener, err := net.FileListener(f)
|
||||
if err != nil {
|
||||
return
|
||||
@ -575,6 +624,22 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
|
||||
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
|
||||
debugPrint("Listening and serving QUIC on %s\n", addr)
|
||||
defer func() { debugPrintError(err) }()
|
||||
|
||||
if engine.isUnsafeTrustedProxies() {
|
||||
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||
}
|
||||
|
||||
err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
|
||||
return
|
||||
}
|
||||
|
||||
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||
// through the specified net.Listener
|
||||
func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
@ -586,12 +651,19 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
|
||||
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||
}
|
||||
|
||||
err = http.Serve(listener, engine.Handler())
|
||||
server := &http.Server{ // #nosec G112
|
||||
Handler: engine.Handler(),
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
return
|
||||
}
|
||||
|
||||
// ServeHTTP conforms to the http.Handler interface.
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
engine.routeTreesUpdated.Do(func() {
|
||||
engine.updateRouteTrees()
|
||||
})
|
||||
|
||||
c := engine.pool.Get().(*Context)
|
||||
c.writermem.reset(w)
|
||||
c.Request = req
|
||||
@ -607,17 +679,23 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Disclaimer: You can loop yourself to deal with this, use wisely.
|
||||
func (engine *Engine) HandleContext(c *Context) {
|
||||
oldIndexValue := c.index
|
||||
oldHandlers := c.handlers
|
||||
c.reset()
|
||||
engine.handleHTTPRequest(c)
|
||||
|
||||
c.index = oldIndexValue
|
||||
c.handlers = oldHandlers
|
||||
}
|
||||
|
||||
func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
httpMethod := c.Request.Method
|
||||
rPath := c.Request.URL.Path
|
||||
unescape := false
|
||||
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
|
||||
if engine.UseEscapedPath {
|
||||
rPath = c.Request.URL.EscapedPath()
|
||||
unescape = engine.UnescapePathValues
|
||||
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
rPath = c.Request.URL.RawPath
|
||||
unescape = engine.UnescapePathValues
|
||||
}
|
||||
@ -657,7 +735,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
break
|
||||
}
|
||||
|
||||
if engine.HandleMethodNotAllowed {
|
||||
if engine.HandleMethodNotAllowed && len(t) > 0 {
|
||||
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
|
||||
// containing a list of the target resource's currently supported methods.
|
||||
allowed := make([]string, 0, len(t)-1)
|
||||
@ -704,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||
prefix = sanitizePathChars(prefix)
|
||||
prefix = removeRepeatedChar(prefix, '/')
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
@ -716,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
// sanitizePathChars removes unsafe characters from path strings,
|
||||
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
|
||||
func sanitizePathChars(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
rPath := req.URL.Path
|
||||
|
||||
19
ginS/gins.go
19
ginS/gins.go
@ -12,15 +12,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var internalEngine *gin.Engine
|
||||
|
||||
func engine() *gin.Engine {
|
||||
once.Do(func() {
|
||||
internalEngine = gin.Default()
|
||||
})
|
||||
return internalEngine
|
||||
}
|
||||
var engine = sync.OnceValue(func() *gin.Engine {
|
||||
return gin.Default()
|
||||
})
|
||||
|
||||
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
|
||||
func LoadHTMLGlob(pattern string) {
|
||||
@ -32,6 +26,11 @@ func LoadHTMLFiles(files ...string) {
|
||||
engine().LoadHTMLFiles(files...)
|
||||
}
|
||||
|
||||
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
|
||||
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
|
||||
engine().LoadHTMLFS(fs, patterns...)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
|
||||
func SetHTMLTemplate(templ *template.Template) {
|
||||
engine().SetHTMLTemplate(templ)
|
||||
@ -154,7 +153,7 @@ func RunUnix(file string) (err error) {
|
||||
|
||||
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||
// through the specified file descriptor.
|
||||
// Note: the method will block the calling goroutine indefinitely unless on error happens.
|
||||
// Note: the method will block the calling goroutine indefinitely unless an error happens.
|
||||
func RunFd(fd int) (err error) {
|
||||
return engine().RunFd(fd)
|
||||
}
|
||||
|
||||
246
ginS/gins_test.go
Normal file
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,18 +16,19 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
|
||||
// params[1]=response status (custom compare status) default:"200 OK"
|
||||
// params[2]=response body (custom compare content) default:"it worked"
|
||||
func testRequest(t *testing.T, params ...string) {
|
||||
|
||||
if len(params) == 0 {
|
||||
t.Fatal("url cannot be empty")
|
||||
}
|
||||
@ -40,18 +41,18 @@ func testRequest(t *testing.T, params ...string) {
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
resp, err := client.Get(params[0])
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, ioerr := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, ioerr)
|
||||
require.NoError(t, ioerr)
|
||||
|
||||
var responseStatus = "200 OK"
|
||||
responseStatus := "200 OK"
|
||||
if len(params) > 1 && params[1] != "" {
|
||||
responseStatus = params[1]
|
||||
}
|
||||
|
||||
var responseBody = "it worked"
|
||||
responseBody := "it worked"
|
||||
if len(params) > 2 && params[2] != "" {
|
||||
responseBody = params[2]
|
||||
}
|
||||
@ -69,17 +70,18 @@ func TestRunEmpty(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run())
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
assert.Error(t, router.Run(":8080"))
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:8080/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":8080"))
|
||||
testRequest(t, "http://localhost:8080/example")
|
||||
}
|
||||
|
||||
func TestBadTrustedCIDRs(t *testing.T) {
|
||||
router := New()
|
||||
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
|
||||
require.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
|
||||
}
|
||||
|
||||
/* legacy tests
|
||||
@ -87,7 +89,7 @@ func TestBadTrustedCIDRsForRun(t *testing.T) {
|
||||
os.Setenv("PORT", "")
|
||||
router := New()
|
||||
router.TrustedProxies = []string{"hello/world"}
|
||||
assert.Error(t, router.Run(":8080"))
|
||||
require.Error(t, router.Run(":8080"))
|
||||
}
|
||||
|
||||
func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
|
||||
@ -100,7 +102,7 @@ func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
|
||||
|
||||
go func() {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.Error(t, router.RunUnix(unixTestSocket))
|
||||
require.Error(t, router.RunUnix(unixTestSocket))
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
@ -112,15 +114,15 @@ func TestBadTrustedCIDRsForRunFd(t *testing.T) {
|
||||
router.TrustedProxies = []string{"hello/world"}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
socketFile, err := listener.File()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
go func() {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.Error(t, router.RunFd(int(socketFile.Fd())))
|
||||
require.Error(t, router.RunFd(int(socketFile.Fd())))
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
@ -132,12 +134,12 @@ func TestBadTrustedCIDRsForRunListener(t *testing.T) {
|
||||
router.TrustedProxies = []string{"hello/world"}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.Error(t, router.RunListener(listener))
|
||||
require.Error(t, router.RunListener(listener))
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
@ -148,7 +150,7 @@ func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
|
||||
os.Setenv("PORT", "")
|
||||
router := New()
|
||||
router.TrustedProxies = []string{"hello/world"}
|
||||
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
require.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
}
|
||||
*/
|
||||
|
||||
@ -164,12 +166,12 @@ func TestRunTLS(t *testing.T) {
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
require.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
testRequest(t, "https://localhost:8443/example")
|
||||
}
|
||||
|
||||
func TestPusher(t *testing.T) {
|
||||
var html = template.Must(template.New("https").Parse(`
|
||||
html := template.Must(template.New("https").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Https Test</title>
|
||||
@ -201,7 +203,7 @@ func TestPusher(t *testing.T) {
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
require.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
testRequest(t, "https://localhost:8449/pusher")
|
||||
}
|
||||
|
||||
@ -212,18 +214,19 @@ func TestRunEmptyWithEnv(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run())
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
assert.Error(t, router.Run(":3123"))
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:3123/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":3123"))
|
||||
testRequest(t, "http://localhost:3123/example")
|
||||
}
|
||||
|
||||
func TestRunTooMuchParams(t *testing.T) {
|
||||
router := New()
|
||||
assert.Panics(t, func() {
|
||||
assert.NoError(t, router.Run("2", "2"))
|
||||
require.NoError(t, router.Run("2", "2"))
|
||||
})
|
||||
}
|
||||
|
||||
@ -233,11 +236,12 @@ func TestRunWithPort(t *testing.T) {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.Run(":5150"))
|
||||
}()
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
assert.Error(t, router.Run(":5150"))
|
||||
// Wait for server to be ready with exponential backoff
|
||||
err := waitForServerReady("http://localhost:5150/example", 10)
|
||||
require.NoError(t, err, "server should start successfully")
|
||||
|
||||
require.Error(t, router.Run(":5150"))
|
||||
testRequest(t, "http://localhost:5150/example")
|
||||
}
|
||||
|
||||
@ -257,36 +261,53 @@ func TestUnixSocket(t *testing.T) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
c, err := net.Dial("unix", unixTestSocket)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
scanner := bufio.NewScanner(c)
|
||||
var response string
|
||||
var responseBuilder strings.Builder
|
||||
for scanner.Scan() {
|
||||
response += scanner.Text()
|
||||
responseBuilder.WriteString(scanner.Text())
|
||||
}
|
||||
response := responseBuilder.String()
|
||||
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||
assert.Contains(t, response, "it worked", "resp body should match")
|
||||
}
|
||||
|
||||
func TestBadUnixSocket(t *testing.T) {
|
||||
router := New()
|
||||
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
|
||||
require.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
|
||||
}
|
||||
|
||||
func TestRunQUIC(t *testing.T) {
|
||||
router := New()
|
||||
go func() {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
|
||||
assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
}()
|
||||
|
||||
// have to wait for the goroutine to start and run the server
|
||||
// otherwise the main thread will complete
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
require.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
|
||||
testRequest(t, "https://localhost:8443/example")
|
||||
}
|
||||
|
||||
func TestFileDescriptor(t *testing.T) {
|
||||
router := New()
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
socketFile, err := listener.File()
|
||||
if isWindows() {
|
||||
// not supported by windows, it is unimplemented now
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if socketFile == nil {
|
||||
@ -302,29 +323,30 @@ func TestFileDescriptor(t *testing.T) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
c, err := net.Dial("tcp", listener.Addr().String())
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
scanner := bufio.NewScanner(c)
|
||||
var response string
|
||||
var responseBuilder strings.Builder
|
||||
for scanner.Scan() {
|
||||
response += scanner.Text()
|
||||
responseBuilder.WriteString(scanner.Text())
|
||||
}
|
||||
response := responseBuilder.String()
|
||||
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||
assert.Contains(t, response, "it worked", "resp body should match")
|
||||
}
|
||||
|
||||
func TestBadFileDescriptor(t *testing.T) {
|
||||
router := New()
|
||||
assert.Error(t, router.RunFd(0))
|
||||
require.Error(t, router.RunFd(0))
|
||||
}
|
||||
|
||||
func TestListener(t *testing.T) {
|
||||
router := New()
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
assert.NoError(t, router.RunListener(listener))
|
||||
@ -334,14 +356,15 @@ func TestListener(t *testing.T) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
c, err := net.Dial("tcp", listener.Addr().String())
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
scanner := bufio.NewScanner(c)
|
||||
var response string
|
||||
var responseBuilder strings.Builder
|
||||
for scanner.Scan() {
|
||||
response += scanner.Text()
|
||||
responseBuilder.WriteString(scanner.Text())
|
||||
}
|
||||
response := responseBuilder.String()
|
||||
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||
assert.Contains(t, response, "it worked", "resp body should match")
|
||||
}
|
||||
@ -349,11 +372,11 @@ func TestListener(t *testing.T) {
|
||||
func TestBadListener(t *testing.T) {
|
||||
router := New()
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener, err := net.ListenTCP("tcp", addr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
listener.Close()
|
||||
assert.Error(t, router.RunListener(listener))
|
||||
require.Error(t, router.RunListener(listener))
|
||||
}
|
||||
|
||||
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
||||
@ -377,9 +400,16 @@ func TestConcurrentHandleContext(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
iterations := 200
|
||||
wg.Add(iterations)
|
||||
for i := 0; i < iterations; i++ {
|
||||
for range iterations {
|
||||
go func() {
|
||||
testGetRequestHandler(t, router, "/")
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
|
||||
assert.Equal(t, 200, w.Code, "should get a 200")
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
@ -401,17 +431,6 @@ func TestConcurrentHandleContext(t *testing.T) {
|
||||
// testRequest(t, "http://localhost:8033/example")
|
||||
// }
|
||||
|
||||
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
|
||||
assert.Equal(t, 200, w.Code, "should get a 200")
|
||||
}
|
||||
|
||||
func TestTreeRunDynamicRouting(t *testing.T) {
|
||||
router := New()
|
||||
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
|
||||
@ -561,3 +580,28 @@ func TestTreeRunDynamicRouting(t *testing.T) {
|
||||
func isWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
|
||||
func TestEscapedColon(t *testing.T) {
|
||||
router := New()
|
||||
f := func(u string) {
|
||||
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
|
||||
}
|
||||
f("/r/r\\:r")
|
||||
f("/r/r:r")
|
||||
f("/r/r/:r")
|
||||
f("/r/r/\\:r")
|
||||
f("/r/r/r\\:r")
|
||||
assert.Panics(t, func() {
|
||||
f("\\foo:")
|
||||
})
|
||||
|
||||
router.updateRouteTrees()
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
|
||||
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
|
||||
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
|
||||
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
|
||||
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
|
||||
}
|
||||
|
||||
502
gin_test.go
502
gin_test.go
@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
@ -45,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
|
||||
})
|
||||
router.GET("/raw", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -72,7 +73,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -82,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestH2c(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
ln, err := net.Listen("tcp", localhostIP+":0")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -130,7 +131,7 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -150,7 +151,7 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -177,7 +178,7 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := client.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -197,7 +198,7 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/raw")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -228,7 +229,7 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -248,7 +249,7 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -268,7 +269,7 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := http.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -295,7 +296,7 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
res, err := client.Get(ts.URL + "/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -315,7 +316,116 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -326,31 +436,31 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
||||
|
||||
func TestAddRoute(t *testing.T) {
|
||||
router := New()
|
||||
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
|
||||
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
|
||||
|
||||
assert.Len(t, router.trees, 1)
|
||||
assert.NotNil(t, router.trees.get("GET"))
|
||||
assert.Nil(t, router.trees.get("POST"))
|
||||
assert.NotNil(t, router.trees.get(http.MethodGet))
|
||||
assert.Nil(t, router.trees.get(http.MethodPost))
|
||||
|
||||
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
|
||||
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
|
||||
|
||||
assert.Len(t, router.trees, 2)
|
||||
assert.NotNil(t, router.trees.get("GET"))
|
||||
assert.NotNil(t, router.trees.get("POST"))
|
||||
assert.NotNil(t, router.trees.get(http.MethodGet))
|
||||
assert.NotNil(t, router.trees.get(http.MethodPost))
|
||||
|
||||
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
|
||||
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
|
||||
assert.Len(t, router.trees, 2)
|
||||
}
|
||||
|
||||
func TestAddRouteFails(t *testing.T) {
|
||||
router := New()
|
||||
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
|
||||
assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
|
||||
assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
|
||||
assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) })
|
||||
assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) })
|
||||
|
||||
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
|
||||
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
|
||||
assert.Panics(t, func() {
|
||||
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
|
||||
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
|
||||
})
|
||||
}
|
||||
|
||||
@ -435,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRebuild404Handlers(t *testing.T) {
|
||||
var middleware0 HandlerFunc = func(c *Context) {}
|
||||
var middleware1 HandlerFunc = func(c *Context) {}
|
||||
|
||||
router := New()
|
||||
|
||||
// Initially, allNoRoute should be nil
|
||||
assert.Nil(t, router.allNoRoute)
|
||||
|
||||
// Set NoRoute handlers
|
||||
router.NoRoute(middleware0)
|
||||
assert.Len(t, router.allNoRoute, 1)
|
||||
assert.Len(t, router.noRoute, 1)
|
||||
compareFunc(t, router.allNoRoute[0], middleware0)
|
||||
|
||||
// Add Use middleware should trigger rebuild404Handlers
|
||||
router.Use(middleware1)
|
||||
assert.Len(t, router.allNoRoute, 2)
|
||||
assert.Len(t, router.Handlers, 1)
|
||||
assert.Len(t, router.noRoute, 1)
|
||||
|
||||
// Global middleware should come first
|
||||
compareFunc(t, router.allNoRoute[0], middleware1)
|
||||
compareFunc(t, router.allNoRoute[1], middleware0)
|
||||
}
|
||||
|
||||
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||
@ -492,27 +625,27 @@ func TestListOfRoutes(t *testing.T) {
|
||||
|
||||
assert.Len(t, list, 7)
|
||||
assertRoutePresent(t, list, RouteInfo{
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/favicon.ico",
|
||||
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
|
||||
})
|
||||
assertRoutePresent(t, list, RouteInfo{
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
|
||||
})
|
||||
assertRoutePresent(t, list, RouteInfo{
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/users/",
|
||||
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
|
||||
})
|
||||
assertRoutePresent(t, list, RouteInfo{
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/users/:id",
|
||||
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
|
||||
})
|
||||
assertRoutePresent(t, list, RouteInfo{
|
||||
Method: "POST",
|
||||
Method: http.MethodPost,
|
||||
Path: "/users/:id",
|
||||
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
|
||||
})
|
||||
@ -530,7 +663,7 @@ func TestEngineHandleContext(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
w := PerformRequest(r, "GET", "/")
|
||||
w := PerformRequest(r, http.MethodGet, "/")
|
||||
assert.Equal(t, 301, w.Code)
|
||||
})
|
||||
}
|
||||
@ -547,10 +680,10 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
||||
r.GET("/:count", func(c *Context) {
|
||||
countStr := c.Param("count")
|
||||
count, err := strconv.Atoi(countStr)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
n, err := c.Writer.Write([]byte("."))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
|
||||
switch {
|
||||
@ -563,7 +696,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
|
||||
w := PerformRequest(r, http.MethodGet, "/"+strconv.Itoa(expectValue-1)) // include 0 value
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, expectValue, w.Body.Len())
|
||||
})
|
||||
@ -572,6 +705,165 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
|
||||
assert.Equal(t, int64(expectValue), middlewareCounter)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
|
||||
// given
|
||||
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
|
||||
|
||||
r := New()
|
||||
v1 := r.Group("/v1")
|
||||
{
|
||||
v1.Use(func(c *Context) {
|
||||
atomic.AddInt64(&middlewareCounterV1, 1)
|
||||
})
|
||||
v1.GET("/test", func(c *Context) {
|
||||
atomic.AddInt64(&handlerCounterV1, 1)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
v2 := r.Group("/v2")
|
||||
{
|
||||
v2.GET("/test", func(c *Context) {
|
||||
c.Request.URL.Path = "/v1/test"
|
||||
r.HandleContext(c)
|
||||
}, func(c *Context) {
|
||||
atomic.AddInt64(&handlerCounterV2, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// when
|
||||
responseV1 := PerformRequest(r, "GET", "/v1/test")
|
||||
responseV2 := PerformRequest(r, "GET", "/v2/test")
|
||||
|
||||
// then
|
||||
assert.Equal(t, 200, responseV1.Code)
|
||||
assert.Equal(t, 200, responseV2.Code)
|
||||
assert.Equal(t, int64(2), handlerCounterV1)
|
||||
assert.Equal(t, int64(2), middlewareCounterV1)
|
||||
assert.Equal(t, int64(1), handlerCounterV2)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextNoRouteWithGroupMiddleware(t *testing.T) {
|
||||
// Scenario from issue #1848:
|
||||
// - Engine with no global middleware (gin.New())
|
||||
// - A group with middleware
|
||||
// - A route in that group
|
||||
// - NoRoute handler that redirects via HandleContext
|
||||
// The group middleware should run exactly once per HandleContext call,
|
||||
// not accumulate across redirects.
|
||||
|
||||
var middlewareCount, handlerCount int64
|
||||
|
||||
r := New()
|
||||
grp := r.Group("", func(c *Context) {
|
||||
atomic.AddInt64(&middlewareCount, 1)
|
||||
c.Next()
|
||||
})
|
||||
grp.GET("/target", func(c *Context) {
|
||||
atomic.AddInt64(&handlerCount, 1)
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
r.NoRoute(func(c *Context) {
|
||||
c.Request.URL.Path = "/target"
|
||||
r.HandleContext(c)
|
||||
})
|
||||
|
||||
// Access a non-existent route to trigger NoRoute -> HandleContext
|
||||
w := PerformRequest(r, "GET", "/nonexistent")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
// Middleware and handler should each run exactly once
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&middlewareCount))
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&handlerCount))
|
||||
}
|
||||
|
||||
func TestEngineHandleContextNoRouteWithEngineMiddleware(t *testing.T) {
|
||||
// When engine middleware exists and NoRoute redirects via HandleContext,
|
||||
// verify the handlers run the expected number of times.
|
||||
|
||||
var engineMwCount, groupMwCount, handlerCount int64
|
||||
|
||||
r := New()
|
||||
r.Use(func(c *Context) {
|
||||
atomic.AddInt64(&engineMwCount, 1)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
grp := r.Group("", func(c *Context) {
|
||||
atomic.AddInt64(&groupMwCount, 1)
|
||||
c.Next()
|
||||
})
|
||||
grp.GET("/target", func(c *Context) {
|
||||
atomic.AddInt64(&handlerCount, 1)
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
r.NoRoute(func(c *Context) {
|
||||
c.Request.URL.Path = "/target"
|
||||
r.HandleContext(c)
|
||||
})
|
||||
|
||||
w := PerformRequest(r, "GET", "/nonexistent")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
// Handler and group middleware should each run once (from HandleContext)
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&handlerCount))
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&groupMwCount))
|
||||
// Engine middleware runs twice: once for the NoRoute chain, once for the HandleContext chain
|
||||
// This is expected behavior since HandleContext re-enters the full handler chain
|
||||
assert.Equal(t, int64(2), atomic.LoadInt64(&engineMwCount))
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is Escaped, the %25 is not interpreted as %
|
||||
assert.Equal(t, "foo%252Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
|
||||
r := New()
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
// Path is used, the %25 is interpreted as %
|
||||
assert.Equal(t, "foo%2Fbar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
|
||||
r := New()
|
||||
r.UseEscapedPath = true
|
||||
r.UseRawPath = true
|
||||
r.UnescapePathValues = false
|
||||
|
||||
r.GET("/v1/:path", func(c *Context) {
|
||||
assert.Equal(t, "foo%25bar", c.Param("path"))
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
@ -580,7 +872,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
|
||||
err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||
}
|
||||
|
||||
@ -588,7 +880,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
{
|
||||
err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// valid ipv4 address
|
||||
@ -597,7 +889,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
|
||||
err := r.SetTrustedProxies([]string{"192.168.1.33"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||
}
|
||||
|
||||
@ -605,7 +897,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
{
|
||||
err := r.SetTrustedProxies([]string{"192.168.1.256"})
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// valid ipv6 address
|
||||
@ -613,7 +905,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
|
||||
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||
}
|
||||
|
||||
@ -621,7 +913,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
{
|
||||
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// valid ipv6 cidr
|
||||
@ -629,7 +921,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
|
||||
err := r.SetTrustedProxies([]string{"::/0"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||
}
|
||||
|
||||
@ -637,7 +929,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
{
|
||||
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// valid combination
|
||||
@ -653,7 +945,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
"172.16.0.1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
|
||||
}
|
||||
|
||||
@ -665,7 +957,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
"172.16.0.256",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// nil value
|
||||
@ -673,7 +965,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||
err := r.SetTrustedProxies(nil)
|
||||
|
||||
assert.Nil(t, r.trustedCIDRs)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -699,7 +991,7 @@ func handlerTest1(c *Context) {}
|
||||
func handlerTest2(c *Context) {}
|
||||
|
||||
func TestNewOptionFunc(t *testing.T) {
|
||||
var fc = func(e *Engine) {
|
||||
fc := func(e *Engine) {
|
||||
e.GET("/test1", handlerTest1)
|
||||
e.GET("/test2", handlerTest2)
|
||||
|
||||
@ -711,8 +1003,8 @@ func TestNewOptionFunc(t *testing.T) {
|
||||
r := New(fc)
|
||||
|
||||
routes := r.Routes()
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||
}
|
||||
|
||||
func TestWithOptionFunc(t *testing.T) {
|
||||
@ -728,14 +1020,14 @@ func TestWithOptionFunc(t *testing.T) {
|
||||
})
|
||||
|
||||
routes := r.Routes()
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||
}
|
||||
|
||||
type Birthday string
|
||||
|
||||
func (b *Birthday) UnmarshalParam(param string) error {
|
||||
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
||||
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -748,9 +1040,119 @@ func TestCustomUnmarshalStruct(t *testing.T) {
|
||||
_ = ctx.BindQuery(&request)
|
||||
ctx.JSON(200, request.Birthday)
|
||||
})
|
||||
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/test?birthday=2000-01-01", nil)
|
||||
w := httptest.NewRecorder()
|
||||
route.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, `"2000/01/01"`, w.Body.String())
|
||||
}
|
||||
|
||||
// Test the fix for https://github.com/gin-gonic/gin/issues/4002
|
||||
func TestMethodNotAllowedNoRoute(t *testing.T) {
|
||||
g := New()
|
||||
g.HandleMethodNotAllowed = true
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
|
||||
assert.Equal(t, http.StatusNotFound, resp.Code)
|
||||
}
|
||||
|
||||
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
|
||||
func TestLiteralColonWithRun(t *testing.T) {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
|
||||
router.GET(`/test\:action`, func(c *Context) {
|
||||
c.JSON(http.StatusOK, H{"path": "literal_colon"})
|
||||
})
|
||||
|
||||
router.updateRouteTrees()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "literal_colon")
|
||||
}
|
||||
|
||||
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
|
||||
router.GET(`/test\:action`, func(c *Context) {
|
||||
c.JSON(http.StatusOK, H{"path": "literal_colon"})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "literal_colon")
|
||||
}
|
||||
|
||||
func TestLiteralColonWithHandler(t *testing.T) {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
|
||||
router.GET(`/test\:action`, func(c *Context) {
|
||||
c.JSON(http.StatusOK, H{"path": "literal_colon"})
|
||||
})
|
||||
|
||||
handler := router.Handler()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "literal_colon")
|
||||
}
|
||||
|
||||
func TestLiteralColonWithHTTPServer(t *testing.T) {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
|
||||
router.GET(`/test\:action`, func(c *Context) {
|
||||
c.JSON(http.StatusOK, H{"path": "literal_colon"})
|
||||
})
|
||||
|
||||
router.GET("/test/:param", func(c *Context) {
|
||||
c.JSON(http.StatusOK, H{"param": c.Param("param")})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "literal_colon")
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
|
||||
router.ServeHTTP(w2, req2)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w2.Code)
|
||||
assert.Contains(t, w2.Body.String(), "foo")
|
||||
}
|
||||
|
||||
// Test that updateRouteTrees is called only once
|
||||
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
|
||||
router.GET(`/test\:action`, func(c *Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
for range 5 {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,10 +10,12 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
@ -295,9 +297,9 @@ func TestShouldBindUri(t *testing.T) {
|
||||
}
|
||||
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||
var person Person
|
||||
assert.NoError(t, c.ShouldBindUri(&person))
|
||||
assert.True(t, person.Name != "")
|
||||
assert.True(t, person.ID != "")
|
||||
require.NoError(t, c.ShouldBindUri(&person))
|
||||
assert.NotEmpty(t, person.Name)
|
||||
assert.NotEmpty(t, person.ID)
|
||||
c.String(http.StatusOK, "ShouldBindUri test OK")
|
||||
})
|
||||
|
||||
@ -317,9 +319,9 @@ func TestBindUri(t *testing.T) {
|
||||
}
|
||||
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
|
||||
var person Person
|
||||
assert.NoError(t, c.BindUri(&person))
|
||||
assert.True(t, person.Name != "")
|
||||
assert.True(t, person.ID != "")
|
||||
require.NoError(t, c.BindUri(&person))
|
||||
assert.NotEmpty(t, person.Name)
|
||||
assert.NotEmpty(t, person.ID)
|
||||
c.String(http.StatusOK, "BindUri test OK")
|
||||
})
|
||||
|
||||
@ -338,7 +340,7 @@ func TestBindUriError(t *testing.T) {
|
||||
}
|
||||
router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
|
||||
var m Member
|
||||
assert.Error(t, c.BindUri(&m))
|
||||
require.Error(t, c.BindUri(&m))
|
||||
})
|
||||
|
||||
path1, _ := exampleFromPath("/new/rest/:num")
|
||||
@ -410,7 +412,7 @@ func exampleFromPath(path string) (string, Params) {
|
||||
}
|
||||
if start >= 0 {
|
||||
if c == '/' {
|
||||
value := fmt.Sprint(rand.Intn(100000))
|
||||
value := strconv.Itoa(rand.Intn(100000))
|
||||
params = append(params, Param{
|
||||
Key: path[start:i],
|
||||
Value: value,
|
||||
@ -424,7 +426,7 @@ func exampleFromPath(path string) (string, Params) {
|
||||
}
|
||||
}
|
||||
if start >= 0 {
|
||||
value := fmt.Sprint(rand.Intn(100000))
|
||||
value := strconv.Itoa(rand.Intn(100000))
|
||||
params = append(params, Param{
|
||||
Key: path[start:],
|
||||
Value: value,
|
||||
|
||||
49
go.mod
49
go.mod
@ -1,38 +1,45 @@
|
||||
module github.com/gin-gonic/gin
|
||||
|
||||
go 1.20
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ugorji/go/codec v1.2.12
|
||||
golang.org/x/net v0.25.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.1
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
golang.org/x/net v0.52.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
)
|
||||
|
||||
106
go.sum
106
go.sum
@ -1,35 +1,42 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@ -39,46 +46,51 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package bytesconv
|
||||
|
||||
import (
|
||||
@ -1,26 +0,0 @@
|
||||
// Copyright 2020 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 !go1.20
|
||||
|
||||
package bytesconv
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StringToBytes converts string to byte slice without a memory allocation.
|
||||
func StringToBytes(s string) []byte {
|
||||
return *(*[]byte)(unsafe.Pointer(
|
||||
&struct {
|
||||
string
|
||||
Cap int
|
||||
}{s, len(s)},
|
||||
))
|
||||
}
|
||||
|
||||
// BytesToString converts byte slice to string without a memory allocation.
|
||||
func BytesToString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
@ -6,14 +6,17 @@ package bytesconv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
cRand "crypto/rand"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
var testBytes = []byte(testString)
|
||||
var (
|
||||
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
testBytes = []byte(testString)
|
||||
)
|
||||
|
||||
func rawBytesToStr(b []byte) string {
|
||||
return string(b)
|
||||
@ -27,14 +30,26 @@ func rawStrToBytes(s string) []byte {
|
||||
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Read(data)
|
||||
for range 100 {
|
||||
_, err := cRand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rawBytesToStr(data) != BytesToString(data) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToStringEmpty(t *testing.T) {
|
||||
if got := BytesToString([]byte{}); got != "" {
|
||||
t.Fatalf("BytesToString([]byte{}) = %q; want empty string", got)
|
||||
}
|
||||
if got := BytesToString(nil); got != "" {
|
||||
t.Fatalf("BytesToString(nil) = %q; want empty string", got)
|
||||
}
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
@ -42,7 +57,7 @@ const (
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
var src = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
sb := strings.Builder{}
|
||||
@ -64,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
}
|
||||
|
||||
func TestStringToBytes(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
s := RandStringBytesMaskImprSrcSB(64)
|
||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||
t.Fatal("don't match")
|
||||
@ -72,28 +87,38 @@ func TestStringToBytes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringToBytesEmpty(t *testing.T) {
|
||||
b := StringToBytes("")
|
||||
if len(b) != 0 {
|
||||
t.Fatalf(`StringToBytes("") length = %d; want 0`, len(b))
|
||||
}
|
||||
if !bytes.Equal(b, []byte("")) {
|
||||
t.Fatalf(`StringToBytes("") = %v; want []byte("")`, b)
|
||||
}
|
||||
}
|
||||
|
||||
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||
|
||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
rawBytesToStr(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
BytesToString(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
rawStrToBytes(testString)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
StringToBytes(testString)
|
||||
}
|
||||
}
|
||||
|
||||
21
internal/fs/fs.go
Normal file
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.
|
||||
Output io.Writer
|
||||
|
||||
// SkipPaths is an url path array which logs are not written.
|
||||
// SkipPaths is a URL path array which logs are not written.
|
||||
// Optional.
|
||||
SkipPaths []string
|
||||
|
||||
// SkipQueryString indicates that query strings should not be written
|
||||
// for cases such as when API keys are passed via query strings.
|
||||
// Optional. Default value is false.
|
||||
SkipQueryString bool
|
||||
|
||||
// Skip is a Skipper that indicates which logs should not be written.
|
||||
// Optional.
|
||||
Skip Skipper
|
||||
@ -82,7 +87,7 @@ type LogFormatterParams struct {
|
||||
// BodySize is the size of the Response Body
|
||||
BodySize int
|
||||
// Keys are the keys set on the request's context.
|
||||
Keys map[string]any
|
||||
Keys map[any]any
|
||||
}
|
||||
|
||||
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
|
||||
@ -103,6 +108,27 @@ func (p *LogFormatterParams) StatusCodeColor() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LatencyColor is the ANSI color for latency
|
||||
func (p *LogFormatterParams) LatencyColor() string {
|
||||
latency := p.Latency
|
||||
switch {
|
||||
case latency < time.Millisecond*100:
|
||||
return white
|
||||
case latency < time.Millisecond*200:
|
||||
return green
|
||||
case latency < time.Millisecond*300:
|
||||
return cyan
|
||||
case latency < time.Millisecond*500:
|
||||
return blue
|
||||
case latency < time.Second:
|
||||
return yellow
|
||||
case latency < time.Second*2:
|
||||
return magenta
|
||||
default:
|
||||
return red
|
||||
}
|
||||
}
|
||||
|
||||
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
|
||||
func (p *LogFormatterParams) MethodColor() string {
|
||||
method := p.Method
|
||||
@ -139,20 +165,27 @@ func (p *LogFormatterParams) IsOutputColor() bool {
|
||||
|
||||
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||
var statusColor, methodColor, resetColor string
|
||||
var statusColor, methodColor, resetColor, latencyColor string
|
||||
if param.IsOutputColor() {
|
||||
statusColor = param.StatusCodeColor()
|
||||
methodColor = param.MethodColor()
|
||||
resetColor = param.ResetColor()
|
||||
latencyColor = param.LatencyColor()
|
||||
}
|
||||
|
||||
if param.Latency > time.Minute {
|
||||
param.Latency = param.Latency.Truncate(time.Second)
|
||||
switch {
|
||||
case param.Latency > time.Minute:
|
||||
param.Latency = param.Latency.Truncate(time.Second * 10)
|
||||
case param.Latency > time.Second:
|
||||
param.Latency = param.Latency.Truncate(time.Millisecond * 10)
|
||||
case param.Latency > time.Millisecond:
|
||||
param.Latency = param.Latency.Truncate(time.Microsecond * 10)
|
||||
}
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
|
||||
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s|%s %8v %s| %15s |%s %-7s %s %#v\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, param.StatusCode, resetColor,
|
||||
param.Latency,
|
||||
latencyColor, param.Latency, resetColor,
|
||||
param.ClientIP,
|
||||
methodColor, param.Method, resetColor,
|
||||
param.Path,
|
||||
@ -270,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||
|
||||
param.BodySize = c.Writer.Size()
|
||||
|
||||
if raw != "" {
|
||||
if raw != "" && !conf.SkipQueryString {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
|
||||
146
logger_test.go
146
logger_test.go
@ -31,31 +31,31 @@ func TestLogger(t *testing.T) {
|
||||
router.HEAD("/example", func(c *Context) {})
|
||||
router.OPTIONS("/example", func(c *Context) {})
|
||||
|
||||
PerformRequest(router, "GET", "/example?a=100")
|
||||
PerformRequest(router, http.MethodGet, "/example?a=100")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
assert.Contains(t, buffer.String(), "a=100")
|
||||
|
||||
// I wrote these first (extending the above) but then realized they are more
|
||||
// like integration tests because they test the whole logging process rather
|
||||
// than individual functions. Im not sure where these should go.
|
||||
// than individual functions. I'm not sure where these should go.
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "POST", "/example")
|
||||
PerformRequest(router, http.MethodPost, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "POST")
|
||||
assert.Contains(t, buffer.String(), http.MethodPost)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "PUT", "/example")
|
||||
PerformRequest(router, http.MethodPut, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "PUT")
|
||||
assert.Contains(t, buffer.String(), http.MethodPut)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "DELETE", "/example")
|
||||
PerformRequest(router, http.MethodDelete, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "DELETE")
|
||||
assert.Contains(t, buffer.String(), http.MethodDelete)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
@ -77,9 +77,9 @@ func TestLogger(t *testing.T) {
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/notfound")
|
||||
PerformRequest(router, http.MethodGet, "/notfound")
|
||||
assert.Contains(t, buffer.String(), "404")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/notfound")
|
||||
}
|
||||
|
||||
@ -95,31 +95,31 @@ func TestLoggerWithConfig(t *testing.T) {
|
||||
router.HEAD("/example", func(c *Context) {})
|
||||
router.OPTIONS("/example", func(c *Context) {})
|
||||
|
||||
PerformRequest(router, "GET", "/example?a=100")
|
||||
PerformRequest(router, http.MethodGet, "/example?a=100")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
assert.Contains(t, buffer.String(), "a=100")
|
||||
|
||||
// I wrote these first (extending the above) but then realized they are more
|
||||
// like integration tests because they test the whole logging process rather
|
||||
// than individual functions. Im not sure where these should go.
|
||||
// than individual functions. I'm not sure where these should go.
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "POST", "/example")
|
||||
PerformRequest(router, http.MethodPost, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "POST")
|
||||
assert.Contains(t, buffer.String(), http.MethodPost)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "PUT", "/example")
|
||||
PerformRequest(router, http.MethodPut, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "PUT")
|
||||
assert.Contains(t, buffer.String(), http.MethodPut)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "DELETE", "/example")
|
||||
PerformRequest(router, http.MethodDelete, "/example")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "DELETE")
|
||||
assert.Contains(t, buffer.String(), http.MethodDelete)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
@ -141,9 +141,9 @@ func TestLoggerWithConfig(t *testing.T) {
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/notfound")
|
||||
PerformRequest(router, http.MethodGet, "/notfound")
|
||||
assert.Contains(t, buffer.String(), "404")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/notfound")
|
||||
}
|
||||
|
||||
@ -169,19 +169,19 @@ func TestLoggerWithFormatter(t *testing.T) {
|
||||
)
|
||||
}))
|
||||
router.GET("/example", func(c *Context) {})
|
||||
PerformRequest(router, "GET", "/example?a=100")
|
||||
PerformRequest(router, http.MethodGet, "/example?a=100")
|
||||
|
||||
// output test
|
||||
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
assert.Contains(t, buffer.String(), "a=100")
|
||||
}
|
||||
|
||||
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||
var gotParam LogFormatterParams
|
||||
var gotKeys map[string]any
|
||||
var gotKeys map[any]any
|
||||
buffer := new(strings.Builder)
|
||||
|
||||
router := New()
|
||||
@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||
gotKeys = c.Keys
|
||||
time.Sleep(time.Millisecond)
|
||||
})
|
||||
PerformRequest(router, "GET", "/example?a=100")
|
||||
PerformRequest(router, http.MethodGet, "/example?a=100")
|
||||
|
||||
// output test
|
||||
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
assert.Contains(t, buffer.String(), http.MethodGet)
|
||||
assert.Contains(t, buffer.String(), "/example")
|
||||
assert.Contains(t, buffer.String(), "a=100")
|
||||
|
||||
@ -225,7 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||
assert.Equal(t, 200, gotParam.StatusCode)
|
||||
assert.NotEmpty(t, gotParam.Latency)
|
||||
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
|
||||
assert.Equal(t, "GET", gotParam.Method)
|
||||
assert.Equal(t, http.MethodGet, gotParam.Method)
|
||||
assert.Equal(t, "/example?a=100", gotParam.Path)
|
||||
assert.Empty(t, gotParam.ErrorMessage)
|
||||
assert.Equal(t, gotKeys, gotParam.Keys)
|
||||
@ -239,7 +239,7 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
StatusCode: 200,
|
||||
Latency: time.Second * 5,
|
||||
ClientIP: "20.20.20.20",
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
ErrorMessage: "",
|
||||
isTerm: false,
|
||||
@ -250,7 +250,7 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
StatusCode: 200,
|
||||
Latency: time.Second * 5,
|
||||
ClientIP: "20.20.20.20",
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
ErrorMessage: "",
|
||||
isTerm: true,
|
||||
@ -260,7 +260,7 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
StatusCode: 200,
|
||||
Latency: time.Millisecond * 9876543210,
|
||||
ClientIP: "20.20.20.20",
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
ErrorMessage: "",
|
||||
isTerm: true,
|
||||
@ -271,17 +271,17 @@ func TestDefaultLogFormatter(t *testing.T) {
|
||||
StatusCode: 200,
|
||||
Latency: time.Millisecond * 9876543210,
|
||||
ClientIP: "20.20.20.20",
|
||||
Method: "GET",
|
||||
Method: http.MethodGet,
|
||||
Path: "/",
|
||||
ErrorMessage: "",
|
||||
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 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
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 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
}
|
||||
|
||||
func TestColorForMethod(t *testing.T) {
|
||||
@ -292,10 +292,10 @@ func TestColorForMethod(t *testing.T) {
|
||||
return p.MethodColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
|
||||
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
|
||||
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
|
||||
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
|
||||
assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue")
|
||||
assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan")
|
||||
assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow")
|
||||
assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red")
|
||||
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
|
||||
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
|
||||
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
|
||||
@ -317,6 +317,24 @@ func TestColorForStatus(t *testing.T) {
|
||||
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||
}
|
||||
|
||||
func TestColorForLatency(t *testing.T) {
|
||||
colorForLatency := func(latency time.Duration) string {
|
||||
p := LogFormatterParams{
|
||||
Latency: latency,
|
||||
}
|
||||
return p.LatencyColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, white, colorForLatency(time.Duration(0)), "0 should be white")
|
||||
assert.Equal(t, white, colorForLatency(time.Millisecond*20), "20ms should be white")
|
||||
assert.Equal(t, green, colorForLatency(time.Millisecond*150), "150ms should be green")
|
||||
assert.Equal(t, cyan, colorForLatency(time.Millisecond*250), "250ms should be cyan")
|
||||
assert.Equal(t, blue, colorForLatency(time.Millisecond*400), "400ms should be blue")
|
||||
assert.Equal(t, yellow, colorForLatency(time.Millisecond*600), "600ms should be yellow")
|
||||
assert.Equal(t, magenta, colorForLatency(time.Millisecond*1500), "1.5s should be magenta")
|
||||
assert.Equal(t, red, colorForLatency(time.Second*3), "other things should be red")
|
||||
}
|
||||
|
||||
func TestResetColor(t *testing.T) {
|
||||
p := LogFormatterParams{}
|
||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
||||
@ -329,13 +347,13 @@ func TestIsOutputColor(t *testing.T) {
|
||||
}
|
||||
|
||||
consoleColorMode = autoColor
|
||||
assert.Equal(t, true, p.IsOutputColor())
|
||||
assert.True(t, p.IsOutputColor())
|
||||
|
||||
ForceConsoleColor()
|
||||
assert.Equal(t, true, p.IsOutputColor())
|
||||
assert.True(t, p.IsOutputColor())
|
||||
|
||||
DisableConsoleColor()
|
||||
assert.Equal(t, false, p.IsOutputColor())
|
||||
assert.False(t, p.IsOutputColor())
|
||||
|
||||
// test with isTerm flag false.
|
||||
p = LogFormatterParams{
|
||||
@ -343,13 +361,13 @@ func TestIsOutputColor(t *testing.T) {
|
||||
}
|
||||
|
||||
consoleColorMode = autoColor
|
||||
assert.Equal(t, false, p.IsOutputColor())
|
||||
assert.False(t, p.IsOutputColor())
|
||||
|
||||
ForceConsoleColor()
|
||||
assert.Equal(t, true, p.IsOutputColor())
|
||||
assert.True(t, p.IsOutputColor())
|
||||
|
||||
DisableConsoleColor()
|
||||
assert.Equal(t, false, p.IsOutputColor())
|
||||
assert.False(t, p.IsOutputColor())
|
||||
|
||||
// reset console color mode.
|
||||
consoleColorMode = autoColor
|
||||
@ -369,15 +387,15 @@ func TestErrorLogger(t *testing.T) {
|
||||
c.String(http.StatusInternalServerError, "hola!")
|
||||
})
|
||||
|
||||
w := PerformRequest(router, "GET", "/error")
|
||||
w := PerformRequest(router, http.MethodGet, "/error")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, "GET", "/abort")
|
||||
w = PerformRequest(router, http.MethodGet, "/abort")
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
|
||||
w = PerformRequest(router, "GET", "/print")
|
||||
w = PerformRequest(router, http.MethodGet, "/print")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||
}
|
||||
@ -389,11 +407,11 @@ func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||
router.GET("/logged", func(c *Context) {})
|
||||
router.GET("/skipped", func(c *Context) {})
|
||||
|
||||
PerformRequest(router, "GET", "/logged")
|
||||
PerformRequest(router, http.MethodGet, "/logged")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/skipped")
|
||||
PerformRequest(router, http.MethodGet, "/skipped")
|
||||
assert.Contains(t, buffer.String(), "")
|
||||
}
|
||||
|
||||
@ -407,11 +425,11 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
||||
router.GET("/logged", func(c *Context) {})
|
||||
router.GET("/skipped", func(c *Context) {})
|
||||
|
||||
PerformRequest(router, "GET", "/logged")
|
||||
PerformRequest(router, http.MethodGet, "/logged")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/skipped")
|
||||
PerformRequest(router, http.MethodGet, "/skipped")
|
||||
assert.Contains(t, buffer.String(), "")
|
||||
}
|
||||
|
||||
@ -427,11 +445,11 @@ func TestLoggerWithConfigSkipper(t *testing.T) {
|
||||
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
|
||||
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
|
||||
|
||||
PerformRequest(router, "GET", "/logged")
|
||||
PerformRequest(router, http.MethodGet, "/logged")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
|
||||
buffer.Reset()
|
||||
PerformRequest(router, "GET", "/skipped")
|
||||
PerformRequest(router, http.MethodGet, "/skipped")
|
||||
assert.Contains(t, buffer.String(), "")
|
||||
}
|
||||
|
||||
@ -454,3 +472,17 @@ func TestForceConsoleColor(t *testing.T) {
|
||||
// reset console color mode.
|
||||
consoleColorMode = autoColor
|
||||
}
|
||||
|
||||
func TestLoggerWithConfigSkipQueryString(t *testing.T) {
|
||||
buffer := new(strings.Builder)
|
||||
router := New()
|
||||
router.Use(LoggerWithConfig(LoggerConfig{
|
||||
Output: buffer,
|
||||
SkipQueryString: true,
|
||||
}))
|
||||
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
|
||||
|
||||
PerformRequest(router, "GET", "/logged?a=21")
|
||||
assert.Contains(t, buffer.String(), "200")
|
||||
assert.NotContains(t, buffer.String(), "a=21")
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
|
||||
signature += " XX "
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
|
||||
signature += " X "
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
|
||||
signature += " XX "
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||
@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
@ -175,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
|
||||
})
|
||||
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
@ -196,14 +196,14 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
||||
c.Next()
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusGone, w.Code)
|
||||
assert.Equal(t, "ACB", signature)
|
||||
}
|
||||
|
||||
// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
|
||||
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
|
||||
// as well as Abort
|
||||
func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||
// SETUP
|
||||
@ -219,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||
signature += "C"
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
@ -246,8 +246,8 @@ func TestMiddlewareWrite(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
w := PerformRequest(router, http.MethodGet, "/")
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
|
||||
}
|
||||
|
||||
16
mode.go
16
mode.go
@ -8,6 +8,7 @@ import (
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
@ -44,8 +45,8 @@ var DefaultWriter io.Writer = os.Stdout
|
||||
var DefaultErrorWriter io.Writer = os.Stderr
|
||||
|
||||
var (
|
||||
ginMode = debugCode
|
||||
modeName = DebugMode
|
||||
ginMode int32 = debugCode
|
||||
modeName atomic.Value
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -65,16 +66,15 @@ func SetMode(value string) {
|
||||
|
||||
switch value {
|
||||
case DebugMode:
|
||||
ginMode = debugCode
|
||||
atomic.StoreInt32(&ginMode, debugCode)
|
||||
case ReleaseMode:
|
||||
ginMode = releaseCode
|
||||
atomic.StoreInt32(&ginMode, releaseCode)
|
||||
case TestMode:
|
||||
ginMode = testCode
|
||||
atomic.StoreInt32(&ginMode, testCode)
|
||||
default:
|
||||
panic("gin mode unknown: " + value + " (available mode: debug release test)")
|
||||
}
|
||||
|
||||
modeName = value
|
||||
modeName.Store(value)
|
||||
}
|
||||
|
||||
// DisableBindValidation closes the default validator.
|
||||
@ -96,5 +96,5 @@ func EnableJsonDecoderDisallowUnknownFields() {
|
||||
|
||||
// Mode returns current gin mode.
|
||||
func Mode() string {
|
||||
return modeName
|
||||
return modeName.Load().(string)
|
||||
}
|
||||
|
||||
19
mode_test.go
19
mode_test.go
@ -5,8 +5,8 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
@ -18,31 +18,24 @@ func init() {
|
||||
}
|
||||
|
||||
func TestSetMode(t *testing.T) {
|
||||
assert.Equal(t, testCode, ginMode)
|
||||
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
|
||||
assert.Equal(t, TestMode, Mode())
|
||||
os.Unsetenv(EnvGinMode)
|
||||
|
||||
SetMode("")
|
||||
assert.Equal(t, testCode, ginMode)
|
||||
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
|
||||
assert.Equal(t, TestMode, Mode())
|
||||
|
||||
tmp := flag.CommandLine
|
||||
flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
|
||||
SetMode("")
|
||||
assert.Equal(t, debugCode, ginMode)
|
||||
assert.Equal(t, DebugMode, Mode())
|
||||
flag.CommandLine = tmp
|
||||
|
||||
SetMode(DebugMode)
|
||||
assert.Equal(t, debugCode, ginMode)
|
||||
assert.Equal(t, int32(debugCode), atomic.LoadInt32(&ginMode))
|
||||
assert.Equal(t, DebugMode, Mode())
|
||||
|
||||
SetMode(ReleaseMode)
|
||||
assert.Equal(t, releaseCode, ginMode)
|
||||
assert.Equal(t, int32(releaseCode), atomic.LoadInt32(&ginMode))
|
||||
assert.Equal(t, ReleaseMode, Mode())
|
||||
|
||||
SetMode(TestMode)
|
||||
assert.Equal(t, testCode, ginMode)
|
||||
assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
|
||||
assert.Equal(t, TestMode, Mode())
|
||||
|
||||
assert.Panics(t, func() { SetMode("unknown") })
|
||||
|
||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
||||
|
||||
package gin
|
||||
|
||||
const stackBufSize = 128
|
||||
|
||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
@ -19,7 +21,6 @@ package gin
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
||||
// removeRepeatedChar removes multiple consecutive 'char's from a string.
|
||||
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
|
||||
func removeRepeatedChar(s string, char byte) string {
|
||||
// Check if there are any consecutive chars
|
||||
hasRepeatedChar := false
|
||||
for i := 1; i < len(s); i++ {
|
||||
if s[i] == char && s[i-1] == char {
|
||||
hasRepeatedChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRepeatedChar {
|
||||
return s
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
// Invariants:
|
||||
// reading from s; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
r := 0
|
||||
w := 0
|
||||
|
||||
for n := len(s); r < n; {
|
||||
if s[r] == char {
|
||||
// Write the first char
|
||||
bufApp(&buf, s, w, char)
|
||||
w++
|
||||
r++
|
||||
|
||||
// Skip all consecutive chars
|
||||
for r < n && s[r] == char {
|
||||
r++
|
||||
}
|
||||
} else {
|
||||
// Copy non-char character
|
||||
bufApp(&buf, s, w, s[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise, return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return s[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
55
path_test.go
55
path_test.go
@ -87,14 +87,14 @@ func TestPathCleanMallocs(t *testing.T) {
|
||||
|
||||
for _, test := range cleanTests {
|
||||
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
||||
assert.EqualValues(t, allocs, 0)
|
||||
assert.InDelta(t, 0, allocs, 0.01)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPathClean(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
for _, test := range cleanTests {
|
||||
cleanPath(test.path)
|
||||
}
|
||||
@ -134,12 +134,59 @@ func TestPathCleanLong(t *testing.T) {
|
||||
|
||||
func BenchmarkPathCleanLong(b *testing.B) {
|
||||
cleanTests := genLongPaths()
|
||||
b.ResetTimer()
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
for _, test := range cleanTests {
|
||||
cleanPath(test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepeatedChar(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
str string
|
||||
char byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
str: "",
|
||||
char: 'a',
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "noSlash",
|
||||
str: "abc",
|
||||
char: ',',
|
||||
want: "abc",
|
||||
},
|
||||
{
|
||||
name: "withSlash",
|
||||
str: "/a/b/c/",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "withRepeatedSlashes",
|
||||
str: "/a//b///c////",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "threeSlashes",
|
||||
str: "///",
|
||||
char: '/',
|
||||
want: "/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := removeRepeatedChar(tc.str, tc.char)
|
||||
assert.Equal(t, tc.want, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
125
recovery.go
125
recovery.go
@ -5,25 +5,27 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
|
||||
var (
|
||||
dunno = []byte("???")
|
||||
centerDot = []byte("·")
|
||||
dot = []byte(".")
|
||||
slash = []byte("/")
|
||||
const (
|
||||
dunno = "???"
|
||||
stackSkip = 3
|
||||
)
|
||||
|
||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||
@ -55,47 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if rec := recover(); rec != nil {
|
||||
// Check for a broken connection, as it is not really a
|
||||
// condition that warrants a panic stack trace.
|
||||
var brokenPipe bool
|
||||
if ne, ok := err.(*net.OpError); ok {
|
||||
var se *os.SyscallError
|
||||
if errors.As(ne, &se) {
|
||||
seStr := strings.ToLower(se.Error())
|
||||
if strings.Contains(seStr, "broken pipe") ||
|
||||
strings.Contains(seStr, "connection reset by peer") {
|
||||
brokenPipe = true
|
||||
}
|
||||
}
|
||||
var isBrokenPipe bool
|
||||
err, ok := rec.(error)
|
||||
if ok {
|
||||
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
|
||||
errors.Is(err, syscall.ECONNRESET) ||
|
||||
errors.Is(err, http.ErrAbortHandler)
|
||||
}
|
||||
if logger != nil {
|
||||
stack := stack(3)
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
headers := strings.Split(string(httpRequest), "\r\n")
|
||||
for idx, header := range headers {
|
||||
current := strings.Split(header, ":")
|
||||
if current[0] == "Authorization" {
|
||||
headers[idx] = current[0] + ": *"
|
||||
}
|
||||
}
|
||||
headersToStr := strings.Join(headers, "\r\n")
|
||||
if brokenPipe {
|
||||
logger.Printf("%s\n%s%s", err, headersToStr, reset)
|
||||
if isBrokenPipe {
|
||||
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
|
||||
} else if IsDebugging() {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||
timeFormat(time.Now()), headersToStr, err, stack, reset)
|
||||
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
|
||||
} else {
|
||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||
timeFormat(time.Now()), err, stack, reset)
|
||||
timeFormat(time.Now()), rec, stack(stackSkip), reset)
|
||||
}
|
||||
}
|
||||
if brokenPipe {
|
||||
if isBrokenPipe {
|
||||
// If the connection is dead, we can't write a status to it.
|
||||
c.Error(err.(error)) //nolint: errcheck
|
||||
c.Error(err) //nolint: errcheck
|
||||
c.Abort()
|
||||
} else {
|
||||
handle(c, err)
|
||||
handle(c, rec)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -103,6 +91,21 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
|
||||
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
|
||||
//
|
||||
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
|
||||
func secureRequestDump(r *http.Request) string {
|
||||
httpRequest, _ := httputil.DumpRequest(r, false)
|
||||
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, "Authorization:") {
|
||||
lines[i] = "Authorization: *"
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\r\n")
|
||||
}
|
||||
|
||||
func defaultHandleRecovery(c *Context, _ any) {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
@ -112,8 +115,11 @@ func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
// loaded file.
|
||||
var lines [][]byte
|
||||
var lastFile string
|
||||
var (
|
||||
nLine string
|
||||
lastFile string
|
||||
err error
|
||||
)
|
||||
for i := skip; ; i++ { // Skip the expected number of frames
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
@ -122,34 +128,53 @@ func stack(skip int) []byte {
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := os.ReadFile(file)
|
||||
nLine, err = readNthLine(file, line-1)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunno
|
||||
// readNthLine reads the nth line from the file.
|
||||
// It returns the trimmed content of the line if found,
|
||||
// or an empty string if the line doesn't exist.
|
||||
// If there's an error opening the file, it returns the error.
|
||||
func readNthLine(file string, n int) (string, error) {
|
||||
if n < 0 {
|
||||
return "", nil
|
||||
}
|
||||
return bytes.TrimSpace(lines[n])
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for i := 0; i < n; i++ {
|
||||
if !scanner.Scan() {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if scanner.Scan() {
|
||||
return strings.TrimSpace(scanner.Text()), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
func function(pc uintptr) []byte {
|
||||
func function(pc uintptr) string {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return dunno
|
||||
}
|
||||
name := []byte(fn.Name())
|
||||
name := fn.Name()
|
||||
// The name includes the path name to the package, which is unnecessary
|
||||
// since the file name is already included. Plus, it has center dots.
|
||||
// That is, we see
|
||||
@ -158,13 +183,13 @@ func function(pc uintptr) []byte {
|
||||
// *T.ptrmethod
|
||||
// Also the package path might contain dot (e.g. code.google.com/...),
|
||||
// so first eliminate the path prefix
|
||||
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
||||
if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
|
||||
name = name[lastSlash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
if period := strings.IndexByte(name, '.'); period >= 0 {
|
||||
name = name[period+1:]
|
||||
}
|
||||
name = bytes.ReplaceAll(name, centerDot, dot)
|
||||
name = strings.ReplaceAll(name, "·", ".")
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
215
recovery_test.go
215
recovery_test.go
@ -5,7 +5,6 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -23,17 +22,17 @@ func TestPanicClean(t *testing.T) {
|
||||
router.Use(RecoveryWithWriter(buffer))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery",
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery",
|
||||
header{
|
||||
Key: "Host",
|
||||
Value: "www.google.com",
|
||||
},
|
||||
header{
|
||||
Key: "Authorization",
|
||||
Value: fmt.Sprintf("Bearer %s", password),
|
||||
Value: "Bearer " + password,
|
||||
},
|
||||
header{
|
||||
Key: "Content-Type",
|
||||
@ -53,21 +52,21 @@ func TestPanicInHandler(t *testing.T) {
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(buffer))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = PerformRequest(router, "GET", "/recovery")
|
||||
w = PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
@ -81,29 +80,14 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
router.Use(RecoveryWithWriter(nil))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
bs := source(nil, 0)
|
||||
assert.Equal(t, dunno, bs)
|
||||
|
||||
in := [][]byte{
|
||||
[]byte("Hello world."),
|
||||
[]byte("Hi, gin.."),
|
||||
}
|
||||
bs = source(in, 10)
|
||||
assert.Equal(t, dunno, bs)
|
||||
|
||||
bs = source(in, 1)
|
||||
assert.Equal(t, []byte("Hello world."), bs)
|
||||
}
|
||||
|
||||
func TestFunction(t *testing.T) {
|
||||
bs := function(1)
|
||||
assert.Equal(t, dunno, bs)
|
||||
@ -114,13 +98,13 @@ func TestFunction(t *testing.T) {
|
||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
expectMsgs := map[syscall.Errno]string{
|
||||
syscall.EPIPE: "broken pipe",
|
||||
syscall.ECONNRESET: "connection reset by peer",
|
||||
expectErrnos := []syscall.Errno{
|
||||
syscall.EPIPE,
|
||||
syscall.ECONNRESET,
|
||||
}
|
||||
|
||||
for errno, expectMsg := range expectMsgs {
|
||||
t.Run(expectMsg, func(t *testing.T) {
|
||||
for _, errno := range expectErrnos {
|
||||
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
|
||||
router := New()
|
||||
@ -135,14 +119,39 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
panic(e)
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, expectCode, w.Code)
|
||||
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
||||
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
|
||||
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
|
||||
func TestPanicWithAbortHandler(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
var buf strings.Builder
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(&buf))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
// Start writing response
|
||||
c.Header("X-Test", "Value")
|
||||
c.Status(expectCode)
|
||||
|
||||
// Panic with ErrAbortHandler which should be treated as broken pipe
|
||||
panic(http.ErrAbortHandler)
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, expectCode, w.Code)
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "net/http: abort Handler")
|
||||
assert.NotContains(t, out, "panic recovered")
|
||||
}
|
||||
|
||||
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
errBuffer := new(strings.Builder)
|
||||
buffer := new(strings.Builder)
|
||||
@ -153,26 +162,26 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
}
|
||||
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = PerformRequest(router, "GET", "/recovery")
|
||||
w = PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -188,26 +197,26 @@ func TestCustomRecovery(t *testing.T) {
|
||||
}
|
||||
router.Use(CustomRecovery(handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = PerformRequest(router, "GET", "/recovery")
|
||||
w = PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -223,26 +232,138 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||
}
|
||||
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, "GET", "/recovery")
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = PerformRequest(router, "GET", "/recovery")
|
||||
w = PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
func TestSecureRequestDump(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
wantContains string
|
||||
wantNotContain string
|
||||
}{
|
||||
{
|
||||
name: "Authorization header standard case",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("Authorization", "Bearer secret-token")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "Bearer secret-token",
|
||||
},
|
||||
{
|
||||
name: "authorization header lowercase",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("authorization", "some-secret")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "some-secret",
|
||||
},
|
||||
{
|
||||
name: "Authorization header mixed case",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("AuThOrIzAtIoN", "token123")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "Authorization: *",
|
||||
wantNotContain: "token123",
|
||||
},
|
||||
{
|
||||
name: "No Authorization header",
|
||||
req: func() *http.Request {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
return r
|
||||
}(),
|
||||
wantContains: "",
|
||||
wantNotContain: "Authorization: *",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := secureRequestDump(tt.req)
|
||||
if tt.wantContains != "" && !strings.Contains(result, tt.wantContains) {
|
||||
t.Errorf("maskHeaders() = %q, want contains %q", result, tt.wantContains)
|
||||
}
|
||||
if tt.wantNotContain != "" && strings.Contains(result, tt.wantNotContain) {
|
||||
t.Errorf("maskHeaders() = %q, want NOT contain %q", result, tt.wantNotContain)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadNthLine tests the readNthLine function with various scenarios.
|
||||
func TestReadNthLine(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
|
||||
tempFile, err := os.CreateTemp("", "testfile*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write test content to the temporary file
|
||||
if _, err := tempFile.WriteString(testContent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tempFile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
lineNum int
|
||||
fileName string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
|
||||
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
|
||||
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
|
||||
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := readNthLine(tt.fileName, tt.lineNum)
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStack(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = stack(stackSkip)
|
||||
}
|
||||
}
|
||||
|
||||
34
render/bson.go
Normal file
34
render/bson.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2025 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// BSON contains the given interface object.
|
||||
type BSON struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
var bsonContentType = []string{"application/bson"}
|
||||
|
||||
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r BSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
|
||||
bytes, err := bson.Marshal(&r.Data)
|
||||
if err == nil {
|
||||
_, err = w.Write(bytes)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
|
||||
func (r BSON) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, bsonContentType)
|
||||
}
|
||||
@ -4,7 +4,10 @@
|
||||
|
||||
package render
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Data contains ContentType and bytes data.
|
||||
type Data struct {
|
||||
@ -15,6 +18,9 @@ type Data struct {
|
||||
// Render (Data) writes data with custom ContentType.
|
||||
func (r Data) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
if len(r.Data) > 0 {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(r.Data)))
|
||||
}
|
||||
_, err = w.Write(r.Data)
|
||||
return
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ package render
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/fs"
|
||||
)
|
||||
|
||||
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
||||
@ -31,10 +33,12 @@ type HTMLProduction struct {
|
||||
|
||||
// HTMLDebug contains template delims and pattern and function with file list.
|
||||
type HTMLDebug struct {
|
||||
Files []string
|
||||
Glob string
|
||||
Delims Delims
|
||||
FuncMap template.FuncMap
|
||||
Files []string
|
||||
Glob string
|
||||
FileSystem http.FileSystem
|
||||
Patterns []string
|
||||
Delims Delims
|
||||
FuncMap template.FuncMap
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
if r.FuncMap == nil {
|
||||
r.FuncMap = template.FuncMap{}
|
||||
@ -73,7 +78,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
if r.Glob != "" {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
|
||||
}
|
||||
panic("the HTML debug render was created without files or glob pattern")
|
||||
if r.FileSystem != nil && len(r.Patterns) > 0 {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
|
||||
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
|
||||
}
|
||||
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
|
||||
}
|
||||
|
||||
// Render (HTML) executes template and writes its result with custom ContentType for response.
|
||||
|
||||
@ -9,9 +9,10 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"unicode"
|
||||
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
// JSON contains the given interface object.
|
||||
@ -65,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
||||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
jsonBytes, err := json.API.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -76,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
|
||||
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -92,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.Marshal(r.Data)
|
||||
jsonBytes, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -115,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
|
||||
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -151,20 +152,23 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
||||
func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
ret, err := json.Marshal(r.Data)
|
||||
ret, err := json.API.Marshal(r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
||||
|
||||
for _, r := range bytesconv.BytesToString(ret) {
|
||||
cvt := string(r)
|
||||
if r >= 128 {
|
||||
cvt = fmt.Sprintf("\\u%04x", int64(r))
|
||||
if r > unicode.MaxASCII {
|
||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
||||
buffer.Write(escapeBuf)
|
||||
} else {
|
||||
buffer.WriteByte(byte(r))
|
||||
}
|
||||
buffer.WriteString(cvt)
|
||||
}
|
||||
|
||||
_, err = w.Write(buffer.Bytes())
|
||||
@ -179,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
|
||||
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
|
||||
func (r PureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder := json.API.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
return encoder.Encode(r.Data)
|
||||
}
|
||||
|
||||
26
render/pdf.go
Normal file
26
render/pdf.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2026 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package render
|
||||
|
||||
import "net/http"
|
||||
|
||||
// PDF contains the given PDF binary data.
|
||||
type PDF struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
var pdfContentType = []string{"application/pdf"}
|
||||
|
||||
// Render (PDF) writes PDF data with custom ContentType.
|
||||
func (r PDF) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
_, err := w.Write(r.Data)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (PDF) writes PDF ContentType for response.
|
||||
func (r PDF) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, pdfContentType)
|
||||
}
|
||||
@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
|
||||
}
|
||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||
}
|
||||
r.writeHeaders(w, r.Headers)
|
||||
r.writeHeaders(w)
|
||||
_, err = io.Copy(w, r.Reader)
|
||||
return
|
||||
}
|
||||
@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, []string{r.ContentType})
|
||||
}
|
||||
|
||||
// writeHeaders writes custom Header.
|
||||
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
|
||||
// writeHeaders writes headers from r.Headers into response.
|
||||
func (r Reader) writeHeaders(w http.ResponseWriter) {
|
||||
header := w.Header()
|
||||
for k, v := range headers {
|
||||
for k, v := range r.Headers {
|
||||
if header.Get(k) == "" {
|
||||
header.Set(k, v)
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ var (
|
||||
_ Render = (*AsciiJSON)(nil)
|
||||
_ Render = (*ProtoBuf)(nil)
|
||||
_ Render = (*TOML)(nil)
|
||||
_ Render = (*PDF)(nil)
|
||||
)
|
||||
|
||||
func writeContentType(w http.ResponseWriter, value []string) {
|
||||
|
||||
@ -7,17 +7,15 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
// TODO unit tests
|
||||
// test errors
|
||||
|
||||
func TestRenderMsgPack(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
@ -29,15 +27,54 @@ func TestRenderMsgPack(t *testing.T) {
|
||||
|
||||
err := (MsgPack{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
h := new(codec.MsgpackHandle)
|
||||
assert.NotNil(t, h)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err = codec.NewEncoder(buf, h).Encode(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, w.Body.String(), buf.String())
|
||||
var decoded map[string]any
|
||||
var mh codec.MsgpackHandle
|
||||
mh.RawToString = true
|
||||
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, data, decoded)
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestWriteMsgPack(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
"foo": "bar",
|
||||
"num": 42,
|
||||
}
|
||||
|
||||
err := WriteMsgPack(w, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
var decoded map[string]any
|
||||
var mh codec.MsgpackHandle
|
||||
mh.RawToString = true
|
||||
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, decoded, 2)
|
||||
assert.Equal(t, "bar", decoded["foo"])
|
||||
assert.EqualValues(t, 42, decoded["num"])
|
||||
}
|
||||
|
||||
type failWriter struct {
|
||||
*httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
func (w *failWriter) Write(data []byte) (int, error) {
|
||||
return 0, errors.New("write error")
|
||||
}
|
||||
|
||||
func TestRenderMsgPackError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
err := (MsgPack{data}).Render(&failWriter{w})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "write error")
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@ -15,15 +16,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// TODO unit tests
|
||||
// test errors
|
||||
|
||||
func TestRenderJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]any{
|
||||
@ -36,8 +35,8 @@ func TestRenderJSON(t *testing.T) {
|
||||
|
||||
err := (JSON{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -46,7 +45,7 @@ func TestRenderJSONError(t *testing.T) {
|
||||
data := make(chan int)
|
||||
|
||||
// json: unsupported type: chan int
|
||||
assert.Error(t, (JSON{data}).Render(w))
|
||||
require.Error(t, (JSON{data}).Render(w))
|
||||
}
|
||||
|
||||
func TestRenderIndentedJSON(t *testing.T) {
|
||||
@ -58,8 +57,8 @@ func TestRenderIndentedJSON(t *testing.T) {
|
||||
|
||||
err := (IndentedJSON{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
|
||||
require.NoError(t, err)
|
||||
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"))
|
||||
}
|
||||
|
||||
@ -69,7 +68,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
|
||||
|
||||
// json: unsupported type: chan int
|
||||
err := (IndentedJSON{data}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderSecureJSON(t *testing.T) {
|
||||
@ -83,8 +82,8 @@ func TestRenderSecureJSON(t *testing.T) {
|
||||
|
||||
err1 := (SecureJSON{"while(1);", data}).Render(w1)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
|
||||
require.NoError(t, err1)
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
@ -95,7 +94,7 @@ func TestRenderSecureJSON(t *testing.T) {
|
||||
}}
|
||||
|
||||
err2 := (SecureJSON{"while(1);", datas}).Render(w2)
|
||||
assert.NoError(t, err2)
|
||||
require.NoError(t, err2)
|
||||
assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -106,7 +105,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
|
||||
|
||||
// json: unsupported type: chan int
|
||||
err := (SecureJSON{"while(1);", data}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderJsonpJSON(t *testing.T) {
|
||||
@ -120,7 +119,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
||||
|
||||
err1 := (JsonpJSON{"x", data}).Render(w1)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
require.NoError(t, err1)
|
||||
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||
|
||||
@ -132,25 +131,50 @@ func TestRenderJsonpJSON(t *testing.T) {
|
||||
}}
|
||||
|
||||
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
||||
assert.NoError(t, err2)
|
||||
require.NoError(t, err2)
|
||||
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
bufString string
|
||||
bufString string
|
||||
ErrThreshold int // 1-based threshold. If 1, errors on the 1st Write call.
|
||||
writeCount int
|
||||
*httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*errorWriter)(nil)
|
||||
|
||||
func (w *errorWriter) Header() http.Header {
|
||||
if w.ResponseRecorder == nil {
|
||||
w.ResponseRecorder = httptest.NewRecorder()
|
||||
}
|
||||
return w.ResponseRecorder.Header()
|
||||
}
|
||||
|
||||
func (w *errorWriter) WriteHeader(statusCode int) {
|
||||
if w.ResponseRecorder == nil {
|
||||
w.ResponseRecorder = httptest.NewRecorder()
|
||||
}
|
||||
w.ResponseRecorder.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *errorWriter) Write(buf []byte) (int, error) {
|
||||
if string(buf) == w.bufString {
|
||||
return 0, errors.New(`write "` + w.bufString + `" error`)
|
||||
w.writeCount++
|
||||
if (w.bufString != "" && string(buf) == w.bufString) || (w.ErrThreshold > 0 && w.writeCount >= w.ErrThreshold) {
|
||||
return 0, errors.New(`write error`)
|
||||
}
|
||||
if w.ResponseRecorder == nil {
|
||||
w.ResponseRecorder = httptest.NewRecorder()
|
||||
}
|
||||
return w.ResponseRecorder.Write(buf)
|
||||
}
|
||||
|
||||
func (w *errorWriter) reset() {
|
||||
w.writeCount = 0
|
||||
w.ResponseRecorder = httptest.NewRecorder()
|
||||
}
|
||||
|
||||
func TestRenderJsonpJSONError(t *testing.T) {
|
||||
ew := &errorWriter{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
@ -163,23 +187,33 @@ func TestRenderJsonpJSONError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cb := template.JSEscapeString(jsonpJSON.Callback)
|
||||
ew.bufString = cb
|
||||
err := jsonpJSON.Render(ew) // error was returned while writing callback
|
||||
assert.Equal(t, `write "`+cb+`" error`, err.Error())
|
||||
// error was returned while writing callback
|
||||
ew.reset()
|
||||
ew.ErrThreshold = 1
|
||||
err := jsonpJSON.Render(ew)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
|
||||
ew.bufString = `(`
|
||||
// error was returned while writing "("
|
||||
ew.reset()
|
||||
ew.ErrThreshold = 2
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
|
||||
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data
|
||||
ew.bufString = string(data)
|
||||
// error was returned while writing data
|
||||
ew.reset()
|
||||
ew.ErrThreshold = 3
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
|
||||
ew.bufString = `);`
|
||||
// error was returned while writing ");"
|
||||
ew.reset()
|
||||
ew.ErrThreshold = 4
|
||||
err = jsonpJSON.Render(ew)
|
||||
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
}
|
||||
|
||||
func TestRenderJsonpJSONError2(t *testing.T) {
|
||||
@ -191,9 +225,9 @@ func TestRenderJsonpJSONError2(t *testing.T) {
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
e := (JsonpJSON{"", data}).Render(w)
|
||||
assert.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"))
|
||||
}
|
||||
|
||||
@ -203,7 +237,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
|
||||
|
||||
// json: unsupported type: chan int
|
||||
err := (JsonpJSON{"x", data}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderAsciiJSON(t *testing.T) {
|
||||
@ -215,15 +249,15 @@ func TestRenderAsciiJSON(t *testing.T) {
|
||||
|
||||
err := (AsciiJSON{data1}).Render(w1)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
|
||||
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
data2 := 3.1415926
|
||||
|
||||
err = (AsciiJSON{data2}).Render(w2)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "3.1415926", w2.Body.String())
|
||||
}
|
||||
|
||||
@ -232,7 +266,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
|
||||
data := make(chan int)
|
||||
|
||||
// json: unsupported type: chan int
|
||||
assert.Error(t, (AsciiJSON{data}).Render(w))
|
||||
require.Error(t, (AsciiJSON{data}).Render(w))
|
||||
}
|
||||
|
||||
func TestRenderPureJSON(t *testing.T) {
|
||||
@ -242,8 +276,8 @@ func TestRenderPureJSON(t *testing.T) {
|
||||
"html": "<b>",
|
||||
}
|
||||
err := (PureJSON{data}).Render(w)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -283,8 +317,15 @@ b:
|
||||
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
err := (YAML{data}).Render(w)
|
||||
assert.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())
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
@ -298,7 +339,7 @@ func (ft *fail) MarshalYAML() (any, error) {
|
||||
func TestRenderYAMLFail(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err := (YAML{&fail{}}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderTOML(t *testing.T) {
|
||||
@ -311,7 +352,7 @@ func TestRenderTOML(t *testing.T) {
|
||||
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
err := (TOML{data}).Render(w)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
|
||||
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -319,7 +360,7 @@ func TestRenderTOML(t *testing.T) {
|
||||
func TestRenderTOMLFail(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err := (TOML{net.IPv4bcast}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// test Protobuf rendering
|
||||
@ -334,12 +375,12 @@ func TestRenderProtoBuf(t *testing.T) {
|
||||
|
||||
(ProtoBuf{data}).WriteContentType(w)
|
||||
protoData, err := proto.Marshal(data)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
|
||||
|
||||
err = (ProtoBuf{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(protoData), w.Body.String())
|
||||
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -348,7 +389,56 @@ func TestRenderProtoBufFail(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := &testdata.Test{}
|
||||
err := (ProtoBuf{data}).Render(w)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderBSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
reps := []int64{int64(1), int64(2)}
|
||||
type mystruct struct {
|
||||
Label string
|
||||
Reps []int64
|
||||
}
|
||||
|
||||
data := &mystruct{
|
||||
Label: "test",
|
||||
Reps: reps,
|
||||
}
|
||||
|
||||
(BSON{data}).WriteContentType(w)
|
||||
bsonData, err := bson.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||
|
||||
err = (BSON{data}).Render(w)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, bsonData, w.Body.Bytes())
|
||||
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderBSONError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := make(chan int)
|
||||
|
||||
err := (BSON{data}).Render(w)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderBSONWriteError(t *testing.T) {
|
||||
type testStruct struct {
|
||||
Value string
|
||||
}
|
||||
data := &testStruct{Value: "test"}
|
||||
|
||||
ew := &errorWriter{
|
||||
ErrThreshold: 1,
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
|
||||
err := (BSON{data}).Render(ew)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
}
|
||||
|
||||
func TestRenderXML(t *testing.T) {
|
||||
@ -362,14 +452,39 @@ func TestRenderXML(t *testing.T) {
|
||||
|
||||
err := (XML{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
|
||||
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderXMLError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := make(chan int)
|
||||
|
||||
err := (XML{data}).Render(w)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "xml: unsupported type: chan int")
|
||||
}
|
||||
|
||||
func TestRenderPDF(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := []byte("%Test pdf content")
|
||||
|
||||
pdf := PDF{data}
|
||||
|
||||
pdf.WriteContentType(w)
|
||||
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
|
||||
|
||||
err := pdf.Render(w)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, data, w.Body.Bytes())
|
||||
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderRedirect(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/test-redirect", nil)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
data1 := Redirect{
|
||||
Code: http.StatusMovedPermanently,
|
||||
@ -379,7 +494,7 @@ func TestRenderRedirect(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
err = data1.Render(w)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
data2 := Redirect{
|
||||
Code: http.StatusOK,
|
||||
@ -390,7 +505,7 @@ func TestRenderRedirect(t *testing.T) {
|
||||
w = httptest.NewRecorder()
|
||||
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
|
||||
err := data2.Render(w)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
data3 := Redirect{
|
||||
@ -401,7 +516,7 @@ func TestRenderRedirect(t *testing.T) {
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
err = data3.Render(w)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// only improve coverage
|
||||
data2.WriteContentType(w)
|
||||
@ -416,9 +531,55 @@ func TestRenderData(t *testing.T) {
|
||||
Data: data,
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "#!PNG some raw data", w.Body.String())
|
||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "19", w.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestRenderDataContentLength(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
size, err := strconv.Atoi(r.URL.Query().Get("size"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := Data{
|
||||
ContentType: "application/octet-stream",
|
||||
Data: make([]byte, size),
|
||||
}
|
||||
assert.NoError(t, data.Render(w))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
for _, size := range []int{0, 1, 100, 100_000} {
|
||||
t.Run(strconv.Itoa(size), func(t *testing.T) {
|
||||
resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type"))
|
||||
assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length"))
|
||||
|
||||
actual, err := io.Copy(io.Discard, resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, size, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDataError(t *testing.T) {
|
||||
ew := &errorWriter{
|
||||
ErrThreshold: 1,
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
data := []byte("#!PNG some raw data")
|
||||
|
||||
err := (Data{
|
||||
ContentType: "image/png",
|
||||
Data: data,
|
||||
}).Render(ew)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
}
|
||||
|
||||
func TestRenderString(t *testing.T) {
|
||||
@ -435,7 +596,7 @@ func TestRenderString(t *testing.T) {
|
||||
Data: []any{"manu", 2},
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hola manu 2", w.Body.String())
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -448,7 +609,7 @@ func TestRenderStringLenZero(t *testing.T) {
|
||||
Data: []any{},
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hola %s %d", w.Body.String())
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -464,7 +625,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
|
||||
|
||||
err := instance.Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -480,7 +641,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
||||
|
||||
err := instance.Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@ -488,10 +649,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
|
||||
func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
htmlRender := HTMLDebug{
|
||||
Files: []string{"../testdata/template/hello.tmpl"},
|
||||
Glob: "",
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
Files: []string{"../testdata/template/hello.tmpl"},
|
||||
Glob: "",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||
"name": "thinkerou",
|
||||
@ -499,7 +662,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||
|
||||
err := instance.Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
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"))
|
||||
}
|
||||
@ -507,10 +670,12 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
|
||||
func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
htmlRender := HTMLDebug{
|
||||
Files: nil,
|
||||
Glob: "../testdata/template/hello*",
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
Files: nil,
|
||||
Glob: "../testdata/template/hello*",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{Left: "{[{", Right: "}]}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
instance := htmlRender.Instance("hello.tmpl", map[string]any{
|
||||
"name": "thinkerou",
|
||||
@ -518,21 +683,70 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
|
||||
|
||||
err := instance.Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
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{
|
||||
"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 TestRenderHTMLDebugPanics(t *testing.T) {
|
||||
htmlRender := HTMLDebug{
|
||||
Files: nil,
|
||||
Glob: "",
|
||||
Delims: Delims{"{{", "}}"},
|
||||
FuncMap: nil,
|
||||
Files: nil,
|
||||
Glob: "",
|
||||
FileSystem: nil,
|
||||
Patterns: nil,
|
||||
Delims: Delims{"{{", "}}"},
|
||||
FuncMap: nil,
|
||||
}
|
||||
assert.Panics(t, func() { htmlRender.Instance("", nil) })
|
||||
}
|
||||
|
||||
func TestRenderHTMLTemplateError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
templ := template.Must(template.New("t").Parse(`Hello {{if .name}}{{.name.DoesNotExist}}{{end}}`))
|
||||
|
||||
htmlRender := HTMLProduction{Template: templ}
|
||||
instance := htmlRender.Instance("t", map[string]any{
|
||||
"name": "alexandernyquist",
|
||||
})
|
||||
|
||||
err := instance.Render(w)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderHTMLTemplateExecuteError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
templ := template.Must(template.New("t").Parse(`Hello {{.name.invalid}}`))
|
||||
|
||||
htmlRender := HTMLProduction{Template: templ}
|
||||
instance := htmlRender.Instance("t", map[string]any{
|
||||
"name": "alexandernyquist",
|
||||
})
|
||||
|
||||
err := instance.Render(w)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRenderReader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@ -548,7 +762,7 @@ func TestRenderReader(t *testing.T) {
|
||||
Headers: headers,
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, body, w.Body.String())
|
||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
|
||||
@ -571,7 +785,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
||||
Headers: headers,
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, body, w.Body.String())
|
||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||
assert.NotContains(t, "Content-Length", w.Header())
|
||||
@ -580,14 +794,14 @@ func TestRenderReaderNoContentLength(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenderWriteError(t *testing.T) {
|
||||
data := []interface{}{"value1", "value2"}
|
||||
data := []any{"value1", "value2"}
|
||||
prefix := "my-prefix:"
|
||||
r := SecureJSON{Data: data, Prefix: prefix}
|
||||
ew := &errorWriter{
|
||||
bufString: prefix,
|
||||
ErrThreshold: 1,
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
err := r.Render(ew)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, `write "my-prefix:" error`, err.Error())
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "write error", err.Error())
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ type TOML struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
var TOMLContentType = []string{"application/toml; charset=utf-8"}
|
||||
var tomlContentType = []string{"application/toml; charset=utf-8"}
|
||||
|
||||
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r TOML) Render(w http.ResponseWriter) error {
|
||||
@ -32,5 +32,5 @@ func (r TOML) Render(w http.ResponseWriter) error {
|
||||
|
||||
// WriteContentType (TOML) writes TOML ContentType for response.
|
||||
func (r TOML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, TOMLContentType)
|
||||
writeContentType(w, tomlContentType)
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ package render
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
// YAML contains the given interface object.
|
||||
|
||||
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -16,6 +17,8 @@ const (
|
||||
defaultStatus = http.StatusOK
|
||||
)
|
||||
|
||||
var errHijackAlreadyWritten = errors.New("gin: response body already written")
|
||||
|
||||
// ResponseWriter ...
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
@ -106,6 +109,11 @@ func (w *responseWriter) Written() bool {
|
||||
|
||||
// Hijack implements the http.Hijacker interface.
|
||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
// Allow hijacking before any data is written (size == -1) or after headers are written (size == 0),
|
||||
// but not after body data is written (size > 0). For compatibility with websocket libraries (e.g., github.com/coder/websocket)
|
||||
if w.size > 0 {
|
||||
return nil, nil, errHijackAlreadyWritten
|
||||
}
|
||||
if w.size < 0 {
|
||||
w.size = 0
|
||||
}
|
||||
@ -120,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
||||
// Flush implements the http.Flusher interface.
|
||||
func (w *responseWriter) Flush() {
|
||||
w.WriteHeaderNow()
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||
|
||||
@ -5,11 +5,14 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO
|
||||
@ -95,13 +98,13 @@ func TestResponseWriterWrite(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Status())
|
||||
assert.Equal(t, http.StatusOK, testWriter.Code)
|
||||
assert.Equal(t, "hola", testWriter.Body.String())
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
n, err = w.Write([]byte(" adios"))
|
||||
assert.Equal(t, 6, n)
|
||||
assert.Equal(t, 10, w.Size())
|
||||
assert.Equal(t, "hola adios", testWriter.Body.String())
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestResponseWriterHijack(t *testing.T) {
|
||||
@ -112,7 +115,7 @@ func TestResponseWriterHijack(t *testing.T) {
|
||||
|
||||
assert.Panics(t, func() {
|
||||
_, _, err := w.Hijack()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
assert.True(t, w.Written())
|
||||
|
||||
@ -123,6 +126,132 @@ func TestResponseWriterHijack(t *testing.T) {
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
type mockHijacker struct {
|
||||
*httptest.ResponseRecorder
|
||||
hijacked bool
|
||||
}
|
||||
|
||||
// Hijack implements the http.Hijacker interface. It just records that it was called.
|
||||
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
m.hijacked = true
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func TestResponseWriterHijackAfterWrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(w ResponseWriter) error // Action to perform before hijacking
|
||||
expectWrittenBeforeHijack bool
|
||||
expectHijackSuccess bool
|
||||
expectWrittenAfterHijack bool
|
||||
expectError error
|
||||
}{
|
||||
{
|
||||
name: "hijack before write should succeed",
|
||||
action: func(w ResponseWriter) error { return nil },
|
||||
expectWrittenBeforeHijack: false,
|
||||
expectHijackSuccess: true,
|
||||
expectWrittenAfterHijack: true, // Hijack itself marks the writer as written
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "hijack after write should fail",
|
||||
action: func(w ResponseWriter) error {
|
||||
_, err := w.Write([]byte("test"))
|
||||
return err
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: false,
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: errHijackAlreadyWritten,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
|
||||
writer := &responseWriter{}
|
||||
writer.reset(hijacker)
|
||||
w := ResponseWriter(writer)
|
||||
|
||||
// Check initial state
|
||||
assert.False(t, w.Written(), "should not be written initially")
|
||||
|
||||
// Perform pre-hijack action
|
||||
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
|
||||
|
||||
// Check state before hijacking
|
||||
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
|
||||
|
||||
// Attempt to hijack
|
||||
_, _, hijackErr := w.Hijack()
|
||||
|
||||
// Check results
|
||||
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
|
||||
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
|
||||
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
|
||||
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(w ResponseWriter) error
|
||||
expectWrittenBeforeHijack bool
|
||||
expectHijackSuccess bool
|
||||
expectWrittenAfterHijack bool
|
||||
expectError error
|
||||
}{
|
||||
{
|
||||
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
|
||||
action: func(w ResponseWriter) error {
|
||||
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
|
||||
return nil
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "hijack after WriteHeaderNow + Write should fail",
|
||||
action: func(w ResponseWriter) error {
|
||||
w.WriteHeaderNow()
|
||||
_, err := w.Write([]byte("test"))
|
||||
return err
|
||||
},
|
||||
expectWrittenBeforeHijack: true,
|
||||
expectHijackSuccess: false,
|
||||
expectWrittenAfterHijack: true,
|
||||
expectError: errHijackAlreadyWritten,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
|
||||
writer := &responseWriter{}
|
||||
writer.reset(hijacker)
|
||||
w := ResponseWriter(writer)
|
||||
|
||||
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
|
||||
|
||||
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
|
||||
|
||||
_, _, hijackErr := w.Hijack()
|
||||
|
||||
if tc.expectError == nil {
|
||||
require.NoError(t, hijackErr, "expected hijack to succeed")
|
||||
} else {
|
||||
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
|
||||
}
|
||||
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
|
||||
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseWriterFlush(t *testing.T) {
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writer := &responseWriter{}
|
||||
@ -135,7 +264,7 @@ func TestResponseWriterFlush(t *testing.T) {
|
||||
|
||||
// should return 500
|
||||
resp, err := http.Get(testServer.URL)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user