Compare commits

...

107 Commits

Author SHA1 Message Date
Abhiyan Khanal
d3ffc99852
test(engine): add regression tests for HandleContext with NoRoute (#4571)
Add two regression tests for GitHub issue #1848 to verify that
Context.handlers are properly isolated in HandleContext when used
from a NoRoute handler with group and engine middleware.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-16 20:49:59 +08:00
Bo-Yi Wu
ecd26c8835
docs(readme): update benchmark results with latest data (#4583)
- Remove 20 deprecated frameworks from benchmark report
- Add BunRouter and Fiber as newly benchmarked routers
- Add ranked summary table based on GitHub API throughput
- Add memory consumption tables sorted by bytes ascending
- Include all micro and API benchmark results with rankings

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:16:42 +08:00
Bo-Yi Wu
a749e4d33c
docs: replace AUTHORS.md with link to GitHub contributors page (#4582)
AUTHORS.md was a manually maintained list of 400+ contributors that
had no automation to keep it current. Replace it with a link to
GitHub s built-in contributors page which is always up-to-date.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:34:58 +08:00
Bo-Yi Wu
65d1c470ec
docs(benchmarks): update benchmark report with Gin v1.12.0 results (#4581)
- Update test environment to Apple M4 Pro, Go 1.25.8, macOS
- Add table of contents for easier navigation
- Add ranked summary tables for each API benchmark
- Reorganize full results by API section with clear headings
- Refresh all memory consumption and benchmark data

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:17:04 +08:00
tsinglua
6d880724cc
refactor(test): use the built-in max/min to simplify the code (#4576)
Signed-off-by: tsinglua <tsinglua@outlook.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-13 22:42:00 +08:00
Bo-Yi Wu
48667a2dd1
chore(deps): upgrade Go dependencies and CI action versions (#4580)
- Bump golangci-lint from v2.9 to v2.11
- Bump aquasecurity/trivy-action from 0.34.2 to 0.35.0
- Upgrade golang.org/x packages (net, crypto, sys, text, arch)
- Upgrade go-json, mimetype, and protobuf to latest patch versions

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:40:18 +08:00
Bo-Yi Wu
d4672219fc
docs: reorganize doc.md TOC into thematic groups (#4579)
The flat TOC under a single "API Examples" heading made it hard to scan
and find relevant content. Reorganize 40+ sections into 7 logical groups
(Routing, Middleware, Logging, Request Binding & Validation, Response
Rendering, Server Configuration, Testing) with description lines for
each group. Move scattered sections into their correct groups and fix
the "avoid-loging-query-strings" anchor typo.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:30:54 +08:00
dependabot[bot]
3e44fdc4d1
chore(deps): bump aquasecurity/trivy-action in the actions group (#4557)
Bumps the actions group with 1 update: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).


Updates `aquasecurity/trivy-action` from 0.34.1 to 0.34.2
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.1...0.34.2)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 14:10:36 +08:00
HolynnChen
cb2b764cc8
test(make): modify test folder and test command in Makefile (#4465)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-28 21:55:28 +08:00
Max Katz
a39670fb7b
docs(license): update license period (#4130)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-28 21:46:37 +08:00
Aum Patel
052d1a79aa
feat(render): add PDF renderer and tests (#4491)
* render: add PDF renderer and tests

* render: update PDF renderer copyright header

* test: add PDF rendering tests including 204 No Content in context_test.go
2026-02-28 21:04:54 +08:00
Bo-Yi Wu
ff00c01e67
docs: revise and expand Gin 1.12.0 release announcement (#4554)
- Update release announcement section to introduce Gin 1.12.0
- Provide a more detailed description of new features and improvements in version 1.12.0

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2026-02-28 20:38:49 +08:00
Bo-Yi Wu
73726dc606
docs: update documentation to reflect Go version changes (#4552)
- Remove the changelog entry about bumping minimum Go version to 1.24 and updating workflows

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2026-02-28 18:10:09 +08:00
Bo-Yi Wu
e292e5caa7
docs: document and finalize Gin v1.12.0 release (#4551)
* docs: document and finalize Gin v1.12.0 release

- Add changelog entries for the Gin v1.12.0 release, covering new features, enhancements, bug fixes, build updates, and dependency upgrades
- Update all historical changelog entries to consistently use the same bullet style
- Bump Gin version constant to v1.12.0
- Update README to announce Gin 1.12.0 instead of 1.11.0

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* Update CHANGELOG.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 18:07:46 +08:00
Bo-Yi Wu
ae3f524974
ci: update Go version support to 1.25+ across CI and docs (#4550)
* ci: update Go version support to 1.25+ across CI and docs

- Remove Go 1.24 from CI test matrix to only support 1.25 and 1.26
- Update documentation to require Go version 1.25 or above

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* chore: increase required Go version for Gin to 1.25

- Update minimum required Go version for Gin from 1.24 to 1.25
- Revise related warning message and test expectations to match new version requirement

Signed-off-by: appleboy <appleboy.tw@gmail.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2026-02-28 17:37:02 +08:00
dependabot[bot]
38534e2bf9
chore(deps): bump golang.org/x/net from 0.50.0 to 0.51.0 (#4548)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.51.0.
- [Commits](https://github.com/golang/net/commits)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-28 16:18:12 +08:00
Varun Chawla
472d086af2
fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath (#4535)
* fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath enabled

When RedirectFixedPath is enabled and a request doesn't match any route,
findCaseInsensitivePathRec panics with "invalid node type". This happens
because it accesses children[0] to find the wildcard child, but addChild()
keeps the wildcard child at the end of the array (not the beginning).

When a node has both static and wildcard children, children[0] is a static
node, so the switch on nType falls through to the default panic case.

The fix mirrors what getValue() already does correctly (line 483):
use children[len(children)-1] to access the wildcard child.

Fixes #2959

* Address review feedback: improve test assertions and prefer static routes in case-insensitive lookup

- Assert found=false for non-matching paths instead of only checking for panics
- Fix expected casing for case-insensitive static route match (/PREFIX/XXX -> /prefix/xxx)
- Update findCaseInsensitivePathRec to try static children before falling back to
  wildcard child, ensuring static routes win over param routes in case-insensitive matching

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-28 11:15:27 +08:00
Mehrdad Banikian
fb2583442c
test(context): use http.StatusContinue constant instead of magic number 100 (#4542)
* refactor(context): use http.StatusContinue constant instead of magic number 100

Replace magic number 100 with http.StatusContinue constant for better
code clarity and maintainability in bodyAllowedForStatus function.

Also fix typo in logger_test.go: colorForLantency -> colorForLatency

Fixes #4489

* test: improve coverage to meet 99% threshold

- Add TestContextGetRawDataNilBody to cover nil body error case
- Add SameSiteDefaultMode test case in SetCookieData
- Add 400ms latency test case for LatencyColor
- Add TestMarshalXMLforHSuccess for successful XML marshaling

Coverage improved from 99.1% to 99.2%

* fix: use require for error assertions (testifylint)
2026-02-28 10:13:11 +08:00
Amirhf
6f1d5fe3cd
test(render): add comprehensive error handling tests (#4541)
* test(render): add comprehensive error handling tests
Add error case tests for XML, Data, BSON, and HTML renderers to improve test coverage and ensure proper error handling:

- TestRenderXMLError: validates XML marshal error handling for unsupported types
- TestRenderDataError: validates Data write error handling
- TestRenderBSONError: validates BSON marshal error handling for unsupported types
- TestRenderBSONWriteError: validates BSON write error handling
- TestRenderHTMLTemplateError: validates HTML template execution error with invalid field access
- TestRenderHTMLTemplateExecuteError: validates HTML template execution error with invalid nested field

All tests pass and maintain 100% coverage for the render package.

* test(render): improve robustness of error handling tests based on PR feedback

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: AmirHossein Fallah <amirhossein.fallah@arvancloud.ir>
2026-02-28 10:11:57 +08:00
Denis Galeev
5c00df8afa
fix(render): write content length in Data.Render (#4206)
* init test

* fix test

* fix assert.EqualValues usage

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-28 10:07:31 +08:00
Jacob McSwain
db309081bc
chore(logger): allow skipping query string output (#4547)
This is useful for APIs that might have sensitive information in the query string, such as API keys.

This patch does not change the default behavior of the code unless the new `SkipQueryString` config option is passed in.

The "skip" term is a bit of a misnomer here, as this doesn't actually skip that log, but modifies the output. I'm open to suggestions for a more appropriate name.

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-27 23:33:46 +08:00
Bob Du
ba093d1947
chore(binding): upgrade bson dependency to mongo-driver v2 (#4549)
Signed-off-by: Bob Du <i@bobdu.cc>
2026-02-27 23:20:01 +08:00
dependabot[bot]
1b414bd54e
chore(deps): bump goreleaser/goreleaser-action in the actions group (#4546)
Bumps the actions group with 1 update: [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action).


Updates `goreleaser/goreleaser-action` from 6 to 7
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 23:18:28 +08:00
dependabot[bot]
81dba46872
chore(deps): bump github.com/go-playground/validator/v10 (#4509)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.28.0 to 10.30.1.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.28.0...v10.30.1)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.30.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 22:39:56 +08:00
dependabot[bot]
0c219e7902
chore(deps): bump aquasecurity/trivy-action in the actions group (#4544)
Bumps the actions group with 1 update: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).


Updates `aquasecurity/trivy-action` from 0.34.0 to 0.34.1
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.0...0.34.1)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 22:33:30 +08:00
Bo-Yi Wu
00900fb3e1
ci: update CI workflows and standardize Trivy config quotes (#4531)
- Update gin workflow to use v2.9 and add Go 1.26 to the matrix
- Upgrade Trivy action to v0.34.0 in the scan workflow
- Change all single quotes to double quotes in Trivy workflow configuration

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-02-21 22:32:32 +08:00
dependabot[bot]
5260de6a83
chore(deps): bump golang.org/x/net from 0.49.0 to 0.50.0 (#4538)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:40:02 +08:00
dependabot[bot]
5f424ff6f6
chore(deps): bump github.com/bytedance/sonic from 1.14.2 to 1.15.0 (#4539)
Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.2 to 1.15.0.
- [Release notes](https://github.com/bytedance/sonic/releases)
- [Commits](https://github.com/bytedance/sonic/compare/v1.14.2...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/bytedance/sonic
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:39:40 +08:00
Amirhf
216a4a7c28
test(render): add comprehensive tests for MsgPack render (#4537)
* test(render): add comprehensive tests for MsgPack render

* test(render): make msgpack tests deterministic

Decode the rendered msgpack output and assert values instead of comparing raw bytes (which can vary with map iteration order).
Enable MsgpackHandle.RawToString so msgpack strings decode as Go strings.

---------

Co-authored-by: AmirHossein Fallah <amirhossein.fallah@arvancloud.ir>
2026-02-18 00:38:36 +08:00
dependabot[bot]
f5c267d2f8
chore(deps): bump aquasecurity/trivy-action in the actions group (#4534)
Bumps the actions group with 1 update: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).


Updates `aquasecurity/trivy-action` from 0.33.1 to 0.34.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 10:58:17 +08:00
dependabot[bot]
bf52b077c8
chore(deps): bump go.mongodb.org/mongo-driver from 1.17.7 to 1.17.9 (#4533)
Bumps [go.mongodb.org/mongo-driver](https://github.com/mongodb/mongo-go-driver) from 1.17.7 to 1.17.9.
- [Release notes](https://github.com/mongodb/mongo-go-driver/releases)
- [Commits](https://github.com/mongodb/mongo-go-driver/compare/v1.17.7...v1.17.9)

---
updated-dependencies:
- dependency-name: go.mongodb.org/mongo-driver
  dependency-version: 1.17.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 10:57:27 +08:00
dependabot[bot]
6e3ac82fa7
chore(deps): bump github.com/quic-go/quic-go from 0.57.1 to 0.59.0 (#4532)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.57.1 to 0.59.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.57.1...v0.59.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 10:57:06 +08:00
dependabot[bot]
71cefce08e
chore(deps): bump github.com/goccy/go-yaml from 1.19.1 to 1.19.2 (#4507)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.1 to 1.19.2.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.1...v1.19.2)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 13:56:40 +08:00
dependabot[bot]
882f42b0ed
chore(deps): bump golang.org/x/net from 0.47.0 to 0.49.0 (#4508)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 13:56:24 +08:00
Varun Chawla
488f8c3ffa
refactor: replace magic numbers with named constants in bodyAllowedForStatus (#4529)
Use http.StatusContinue and http.StatusOK instead of hardcoded 100 and
199 for the 1xx informational status range check, consistent with the
pattern already used in logger.go.

Fixes #4489
2026-02-13 13:55:23 +08:00
Mahan Adhikari
8e07d37c63
fix: Correct typos, improve documentation clarity, and remove dead code (#4511)
* fix: correct typos and improve documentation clarity

- Fix typo "Oupps" to "Oops" in recovery test panic messages
- Fix confusing documentation in Bind() and ShouldBind() methods
  that incorrectly stated "JSON or XML as a JSON input"
- Remove double period in StaticFileFS documentation comment
- Remove unused ErrorTypeNu constant that had duplicate comment
  with ErrorTypeAny and was never used in the codebase

* tech: Fix the pull request routing link
2026-02-13 13:54:14 +08:00
Laurent Caumont
d7776de7d4
feat(render): add bson protocol (#4145) 2026-01-27 10:09:01 +08:00
wanghaolong613
e3118cc378
refactor: for loop can be modernized using range over int (#4392)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-25 00:51:11 +08:00
Artur Melanchyk
cad29c5e3f
perf(tree): reduce allocations in findCaseInsensitivePath (#4417)
Co-authored-by: Artur Melanchyk <13834276+arturmelanchyk@users.noreply.github.com>
2026-01-25 00:46:02 +08:00
dependabot[bot]
d9e5cdf9c6
chore(deps): bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1 (#4476)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 17:55:09 +08:00
Raju Ahmed
53410d2e07
feat(context): add GetError and GetErrorSlice methods for error retrieval (#4502)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-24 17:54:37 +08:00
dependabot[bot]
ac95fa6bbc
chore(deps): bump goreleaser/goreleaser-action from 5 to 6 (#3992)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 15:22:06 +08:00
takanuva15
192ac89eef
feat(binding): add support for encoding.UnmarshalText in uri/query binding (#4203) 2026-01-24 15:20:24 +08:00
WeidiDeng
b2b489dbf4
chore(context): always trust xff headers from unix socket (#3359)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-18 12:56:22 +08:00
OHZEKI Naoki
3ab698dc51
refactor(recovery): smart error comparison (#4142)
* refactor(recovery): rename var in CustomRecoveryWithWriter

* refactor(recovery): smart error comparison

* test(recovery): Directly reference the syscall error string
2026-01-17 16:40:43 +08:00
Nurysso
9914178584
fix(context): ClientIP handling for multiple X-Forwarded-For header values (#4472)
* Fix ClientIP calculation by concatenating all RemoteIPHeaders values

* test: used http.MethodGet instead constants and fix lints

* lint error fixed

* Refactor ClientIP X-Forwarded-For tests

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-02 10:15:27 +08:00
Paulo Henrique
915e4c90d2
refactor(context): replace hardcoded localhost IPs with constants (#4481) 2025-12-27 19:25:17 +08:00
Twacqwq
26c3a62865
chore(response): prevent Flush() panic when http.Flusher (#4479) 2025-12-24 18:35:20 +08:00
dependabot[bot]
22c274c84b
chore(deps): bump actions/cache from 4 to 5 in the actions group (#4469)
Bumps the actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 18:33:46 +08:00
OHZEKI Naoki
d1a15347b1
refactor(utils): move util functions to utils.go (#4467)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-12 13:43:25 +08:00
Name
64a6ed9a41
perf(recovery): optimize line reading in stack function (#4466)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-12-12 13:42:03 +08:00
OHZEKI Naoki
19b877fa50
test(debug): improve the test coverage of debug.go to 100% (#4404) 2025-12-05 11:18:08 +08:00
OHZEKI Naoki
2a794cd0b0
fix(debug): version mismatch (#4403)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-04 10:49:37 +08:00
guonaihong
b917b14ff9
fix(binding): empty value error (#2169)
* fix empty value error

Here is the code that can report an error
```go
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"net/http"
	"os"
	"time"
)

type header struct {
	Duration   time.Duration `header:"duration"`
	CreateTime time.Time     `header:"createTime" time_format:"unix"`
}

func needFix1() {
	g := gin.Default()
	g.GET("/", func(c *gin.Context) {
		h := header{}
		err := c.ShouldBindHeader(&h)
		if err != nil {
			c.JSON(500, fmt.Sprintf("fail:%s\n", err))
			return
		}

		c.JSON(200, h)
	})

	g.Run(":8081")
}

func needFix2() {
	g := gin.Default()
	g.GET("/", func(c *gin.Context) {
		h := header{}
		err := c.ShouldBindHeader(&h)
		if err != nil {
			c.JSON(500, fmt.Sprintf("fail:%s\n", err))
			return
		}

		c.JSON(200, h)
	})

	g.Run(":8082")
}

func sendNeedFix1() {
	// send to needFix1
	sendBadData("http://127.0.0.1:8081", "duration")
}

func sendNeedFix2() {
	// send to needFix2
	sendBadData("http://127.0.0.1:8082", "createTime")
}

func sendBadData(url, key string) {
	req, err := http.NewRequest("GET", "http://127.0.0.1:8081", nil)
	if err != nil {
		fmt.Printf("err:%s\n", err)
		return
	}

	// Only the key and no value can cause an error
	req.Header.Add(key, "")
	rsp, err := http.DefaultClient.Do(req)
	if err != nil {
		return
	}
	io.Copy(os.Stdout, rsp.Body)
	rsp.Body.Close()
}

func main() {
	go needFix1()
	go needFix2()

	time.Sleep(time.Second / 1000 * 200) // 200ms
	sendNeedFix1()
	sendNeedFix2()
}

```

* modify code

* add comment

* test(binding): use 'any' alias and require.NoError in form mapping tests

- Replace 'interface{}' with 'any' alias in bindTestData struct
- Change assert.NoError to require.NoError in TestMappingTimeUnixNano and TestMappingTimeDuration to fail fast on mapping errors

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-03 19:18:10 +08:00
dependabot[bot]
fad706f121
chore(deps): bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0 (#4458)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 20:09:41 +08:00
Wayne Aki
f416d1e594
test(gin): resolve race conditions in integration tests (#4453)
- Implement TestRebuild404Handlers to verify 404 handler chain rebuilding
  when global middleware is added via Use()
- Add waitForServerReady helper with exponential backoff to replace
  unreliable time.Sleep() calls in integration tests
- Fix race conditions in TestRunEmpty, TestRunEmptyWithEnv, and
  TestRunWithPort by using proper server readiness checks
- All tests now pass consistently with -race flag

This addresses the empty test function and eliminates flaky test failures
caused by insufficient wait times for server startup.

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-30 15:38:07 +08:00
Milad
583db590ec
test(bytesconv): add tests for empty/nil cases (#4454)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-30 15:25:46 +08:00
appleboy
af6e8b70b8
chore(deps): upgrade quic-go to v0.57.1
Fix CVE-2025-59530 vulnerability (quic-go Crash Due to Premature HANDSHAKE_DONE Frame)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 11:52:47 +08:00
Yilong Li
63dd3e60ca
fix(recover): suppress http.ErrAbortHandler in recover (#4336)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-27 23:20:52 +08:00
Milad
c358d5656d
test(gin): Add comprehensive test coverage for ginS package (#4442)
* test(ginS): add comprehensive test coverage for ginS package

Improve test coverage for ginS package by adding 18 test functions covering HTTP methods, routing, middleware, static files, and templates.

* use http.Method* constants instead of raw strings in gins_test.go

* copyright updated in gins_test.go

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-27 23:01:57 +08:00
Aeddis Desauw
771dcc6476
feat(gin): add option to use escaped path (#4420)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-27 17:55:34 +08:00
dependabot[bot]
52ecf029bd
chore(deps): bump actions/checkout from 5 to 6 in the actions group (#4446)
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-26 23:33:08 +08:00
Name
440eb14ab8
perf(path): replace regex with custom functions in redirectTrailingSlash (#4414)
* perf: replace regex with custom functions in redirectTrailingSlash

* perf: use more efficient removeRepeatedChar for path slash handling

---------

Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-11-26 23:32:18 +08:00
Bo-Yi Wu
ecb3f7b5e2
chore(deps): upgrade golang.org/x/crypto to v0.45.0 (#4449)
- Update golang.org/x/crypto dependency to version 0.45.0

1. https://avd.aquasec.com/nvd/cve-2025-47914
2. https://avd.aquasec.com/nvd/cve-2025-58181

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-11-23 11:46:13 +08:00
Bo-Yi Wu
e88fc8927a
ci(sec): schedule Trivy security scans to run daily at midnight UTC (#4439)
- Change Trivy scan schedule from quarterly to daily runs at 00:00 UTC

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-11-18 23:05:54 +08:00
Pawan Kalyan
5fad976b37
fix(gin): literal colon routes not working with engine.Handler() (#4415)
* fix: call updateRouteTrees in ServeHTTP using sync.Once to support literal colon routes in all usage scenarios (#4413)

* chore: fixed golangci-lint issue in test cases for literal colon

* fix: gofumpt formatting issue

* fix: gofumpt issue in gin.go

* chore: updated routeTreesUpdated comments

* chore: removed unused variable and updated TestUpdateRouteTreesCalledOnce testcase

* chore: moved tests from literal_colon_test.go into gin_test.go

---------

Co-authored-by: pawannn <pawan@zenz.tech>
2025-11-16 09:22:07 +08:00
Bo-Yi Wu
93ff771e6d
ci(sec): improve type safety and server organization in HTTP middleware (#4437)
- Update linting configuration to exclude G115 gosec check instead of including specific checks
- Add the safeInt8 helper for safer type conversions and use it to prevent int8 overflow in middleware handler execution
- Group related constants and variables together for better organization in gin.go
- Refactor HTTP server instantiation to use a dedicated http.Server object for all Run methods
- Add the safeUint16 helper and use it to safely handle conversions in tree node functions to prevent uint16 overflow

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-11-15 23:03:32 +08:00
AtoriUzawa
58135f06cf
docs(context): add example comments for ShouldBind* methods (#4428)
- Added detailed example for ShouldBindJSON
- Added consistent descriptive comments for ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindTOML, ShouldBindPlain, ShouldBindHeader, ShouldBindUri
- Makes binding method usage clearer for new users
2025-11-15 19:46:45 +08:00
efcking
a85ef5ce4d
refactor: use b.Loop() to simplify the code and improve performance (#4432)
Signed-off-by: efcking <efcking@outlook.com>
2025-11-15 19:22:18 +08:00
Bo-Yi Wu
fb27ef26c2
ci(lint): refactor test assertions and linter configuration (#4436)
- Update golangci-lint GitHub Action version from v2.1.6 to v2.6
- Remove the gci formatter and exclusions for third_party, builtin, and examples from the linter config
- Fix argument order for assert.EqualValues and assert.Exactly in context tests for clarity
- Refactor integration tests to build response strings using strings.Builder instead of direct concatenation for improved performance and readability

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-11-15 19:21:42 +08:00
dependabot[bot]
19c2d5c0d1
chore(deps): bump golang.org/x/net from 0.46.0 to 0.47.0 (#4433)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.46.0 to 0.47.0.
- [Commits](https://github.com/golang/net/compare/v0.46.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-15 12:42:25 +08:00
dependabot[bot]
a9401cd238
chore(deps): bump github.com/quic-go/quic-go from 0.55.0 to 0.56.0 (#4430)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.55.0 to 0.56.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.55.0...v0.56.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-15 12:32:48 +08:00
dependabot[bot]
d1bcabc7ee
chore(deps): bump golangci/golangci-lint-action in the actions group (#4431)
Bumps the actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 8 to 9
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-15 12:32:23 +08:00
Name
c3d5a28ed6
fix(gin): close os.File in RunFd to prevent resource leak (#4422)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-11-07 12:01:19 +08:00
Name
acc55e049e
feat(context): add Protocol Buffers support to content negotiation (#4423)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-11-07 11:59:58 +08:00
dependabot[bot]
0c0e99d253
chore(deps): bump github/codeql-action from 3 to 4 in the actions group (#4425)
Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-07 11:57:41 +08:00
Bo-Yi Wu
dceb61e6e7
docs(README): add a Trivy security scan badge (#4426)
- Add a Trivy security scan badge to the documentation
- Import the log package in the example code
- Improve error handling for server startup in the example code

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-07 11:57:12 +08:00
Bo-Yi Wu
5e5ff3ace4
ci: replace vulnerability scanning workflow with Trivy integration (#4421)
- Remove the vulnerability-scanning job from the gin workflow
- Add a dedicated Trivy security scan workflow with scheduled, push, pull request, and manual triggers
- Improve Trivy scan output by uploading SARIF results to the GitHub Security tab and logging table output

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-06 14:15:50 +08:00
Name
2e22e50859
perf(tree): optimize path parsing using strings.Count (#4246)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-10-31 22:09:07 +08:00
dependabot[bot]
52f70cf18a
chore(deps): bump github.com/ugorji/go/codec from 1.3.0 to 1.3.1 (#4409)
Bumps [github.com/ugorji/go/codec](https://github.com/ugorji/go) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/ugorji/go/releases)
- [Commits](https://github.com/ugorji/go/compare/codec/v1.3.0...codec/v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/ugorji/go/codec
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 22:03:29 +08:00
dependabot[bot]
87c207a140
chore(deps): bump github.com/bytedance/sonic from 1.14.0 to 1.14.2 (#4410)
Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.0 to 1.14.2.
- [Release notes](https://github.com/bytedance/sonic/releases)
- [Commits](https://github.com/bytedance/sonic/compare/v1.14.0...v1.14.2)

---
updated-dependencies:
- dependency-name: github.com/bytedance/sonic
  dependency-version: 1.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 22:02:56 +08:00
wanghaolong613
c0048f645e
refactor(context): omit the return value names (#4395) 2025-10-17 11:39:49 +08:00
Spyder01
38e7651192
feat(context): implemented Delete method
Co-authored-by: suhan <s.bangera@castsoftware.com>
2025-10-17 11:21:34 +08:00
letreturn
c221133ee8
docs(context): fix some comments (#4396)
Signed-off-by: letreturn <letreturn@outlook.com>
2025-10-14 22:37:07 +08:00
Name
c3d1092b3b
fix(binding): improve empty slice/array handling in form binding (#4380)
Co-authored-by: huangzw <huangzw@2345.com>
2025-10-11 19:20:41 +08:00
reddaisyy
9968c4bf9d
refactor: use b.Loop() to simplify the code and improve performance (#4389)
Signed-off-by: reddaisyy <reddaisy@outlook.jp>
2025-10-09 11:36:56 +08:00
dependabot[bot]
053e5765fd
chore(deps): bump github.com/quic-go/quic-go from 0.54.1 to 0.55.0 (#4384)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.1 to 0.55.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.54.1...v0.55.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 11:33:36 +08:00
dependabot[bot]
0d085ed9fe
chore(deps): bump golang.org/x/net from 0.43.0 to 0.46.0 (#4391)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.43.0 to 0.46.0.
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 11:32:58 +08:00
Bo-Yi Wu
5dd833f1f2
chore: bump minimum Go version to 1.24 and update workflows (#4388)
- Update minimum required Go version from 1.23 to 1.24 throughout documentation, warnings, and tests
- Remove Go 1.23 from the GitHub Actions workflow matrix
- Change single quotes to double quotes for consistency in workflow configuration

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-10-08 08:30:45 +08:00
dependabot[bot]
48a5dca087
chore(deps): bump github.com/go-playground/validator/v10 (#4385)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.27.0 to 10.28.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.27.0...v10.28.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 08:13:29 +08:00
dependabot[bot]
0bd10a84f9
chore(deps): bump github/codeql-action from 3 to 4 in the actions group (#4387)
Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 08:12:52 +08:00
ljz
4dec17afdf
feat(logger): color latency (#4146)
Co-authored-by: lizhao <lizhao@qq.com>
2025-10-05 11:23:57 +08:00
goldlinker
731374fb36
docs(context): fix wrong function name in comment (#4382)
Signed-off-by: goldlinker <goldlinker@outlook.jp>
2025-10-03 21:26:47 +08:00
dependabot[bot]
8ca975441f
chore(deps): bump google.golang.org/protobuf from 1.36.9 to 1.36.10 (#4383)
Bumps google.golang.org/protobuf from 1.36.9 to 1.36.10.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 21:25:35 +08:00
russcoss
39858a0859
refactor(binding): use maps.Copy for cleaner map handling (#4352)
Signed-off-by: russcoss <russcoss@outlook.com>
2025-09-27 11:03:59 +08:00
Meng Xun
ed150e7254
test(benchmarks): fix the incorrect function name (#4375)
Signed-off-by: mengxun <mengxun1122@163.com>
2025-09-26 08:15:35 +08:00
Bo-Yi Wu
234a6d4c00
fix(response): refine hijack behavior for response lifecycle (#4373)
* feat: refine hijack behavior for response lifecycle and add tests

- Clarify the error message for attempted hijack after response body data is written
- Modify hijack behavior: allow hijacking after headers are written (for better websocket compatibility), but block hijacking after any body data is sent
- Add comprehensive tests to validate allowed hijack after header write and disallowed hijack after body write

fix https://github.com/gin-gonic/gin/issues/4372

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* test: use require for immediate test failure on errors

- Replace assert with require for error checks to ensure test failures immediately halt execution

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* Update response_writer.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-26 08:13:39 +08:00
dependabot[bot]
df2753778e
chore(deps): bump github.com/quic-go/quic-go from 0.54.0 to 0.54.1 (#4379)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.54.0 to 0.54.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.54.0...v0.54.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.54.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 08:10:02 +08:00
dependabot[bot]
048f6fb884
chore(deps): bump the actions group with 2 updates (#4368)
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

Updates `codecov/codecov-action` from 4 to 5
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 21:18:25 +08:00
Bo-Yi Wu
61b67de522
ci(bot): increase frequency and group updates for dependencies (#4367)
- Change the update schedule for both gomod and GitHub Actions dependencies from weekly to daily
- Add grouping for GitHub Actions updates using a catch-all pattern

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-21 21:11:11 +08:00
appleboy
f3a5e78719
docs: update feature documentation instructions for broken doc link
- Fix a broken link to docs/doc.md in the feature documentation instructions

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-21 17:48:39 +08:00
cui
414de60574
refactor(context): using maps.Clone (#4333)
ref: https://go-review.googlesource.com/c/go/+/471400
2025-09-21 17:46:17 +08:00
Name
59e9d4a794
refactor(ginS): use sync.OnceValue to simplify engine function (#4314)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-09-21 17:41:54 +08:00
Bo-Yi Wu
6a1d1218c3
docs: revamp contributing guidelines with comprehensive instructions (#4365)
- Rewrite and expand the contributing guidelines for clarity and thoroughness
- Add distinct sections for Issues and Pull Requests with step-by-step instructions
- Include links to documentation, user guides, and the discussions forum
- Provide advice for reporting bugs and making feature requests
- Specify requirements for pull requests, including branch, commit count, and test coverage
- Clarify documentation expectations for new features and reference the pull request checklist
- Add guidance for security-related bug reports and communication language

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-21 17:39:33 +08:00
Bo-Yi Wu
7925414704
docs: revamp GitHub contribution and support templates (#4364)
- Replace the old issue template with new, structured YAML templates for bug reports and feature requests
- Add a configuration file that directs users to relevant documentation and support links
- Update the pull request template to use a checklist format and clarify documentation requirements

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-21 12:48:19 +08:00
Bo-Yi Wu
1bbbec0baf
docs: announce Gin 1.11.0 release with blog link (#4363)
- Add a new section announcing Gin 1.11.0 and link to its release blog post

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-21 10:50:09 +08:00
Bo-Yi Wu
4dd00f81b1
docs(readme): revamp and expand documentation for clarity and completeness (#4362)
- Update contributing header to single hash style
- Remove deprecated badge and improve project summary wording
- Reorganize and clarify feature descriptions and benefits
- Restructure getting started and installation instructions for clarity
- Expand and annotate the first example application walkthrough
- Detail the steps for running the sample application and expected output
- Improve guidance on learning resources and example projects
- Enhance API reference, documentation links, and tutorial references
- Add a clear performance benchmarks section comparing Gin to other frameworks
- Expand middleware section with ecosystem highlights and usage details
- Create a production usage section listing notable projects using Gin
- Revamp contribution section with clearer procedure and encouragement for new contributors

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-20 20:58:46 +08:00
63 changed files with 4205 additions and 2436 deletions

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@ -24,16 +24,16 @@ jobs:
with:
go-version: "^1"
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9
with:
version: v2.1.6
version: v2.11
args: --verbose
test:
needs: lint
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.23", "1.24", "1.25"]
go: ["1.25", "1.26"]
test-tags:
[
"",
@ -61,11 +61,11 @@ jobs:
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 }}
@ -78,22 +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 }}
vulnerability-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner in repo mode
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: 'fs'
ignore-unfixed: true
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH,MEDIUM'

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
@ -21,7 +21,7 @@ jobs:
with:
go-version: "^1"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser

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

@ -0,0 +1,56 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: "0 0 * * *"
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.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"

View File

@ -18,15 +18,8 @@ linters:
- wastedassign
settings:
gosec:
includes:
- G102
- G106
- G108
- G109
- G111
- G112
- G201
- G203
excludes:
- G115
perfsprint:
int-conversion: true
err-error: true
@ -68,7 +61,6 @@ linters:
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
@ -80,7 +72,4 @@ formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- gin.go

View File

@ -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>

View File

@ -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 |

View File

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

View File

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

View File

@ -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

View File

@ -4,7 +4,7 @@ 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
@ -12,7 +12,11 @@ TESTTAGS ?= ""
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; \

189
README.md
View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
}
func TestBindingJSONNilBody(t *testing.T) {
@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:]))
}
func TestBindingBSON(t *testing.T) {
var obj FooStruct
obj.Foo = "bar"
data, _ := bson.Marshal(&obj)
testBodyBinding(t,
BSON, "bson",
"/", "/",
string(data),
// note: for badbody, we remove first byte to make it invalid
string(data[1:]))
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)

30
binding/bson.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"io"
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
type bsonBinding struct{}
func (bsonBinding) Name() string {
return "bson"
}
func (b bsonBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err == nil {
err = b.BindBody(buf, obj)
}
return err
}
func (bsonBinding) BindBody(body []byte, obj any) error {
return bson.Unmarshal(body, obj)
}

View File

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

View File

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

View File

@ -5,8 +5,10 @@
package binding
import (
"encoding"
"errors"
"fmt"
"maps"
"mime/multipart"
"reflect"
"strconv"
@ -117,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
tValue := value.Type()
var isSet bool
for i := 0; i < value.NumField(); i++ {
for i := range value.NumField() {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
@ -136,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
type setOptions struct {
isDefaultExists bool
defaultValue string
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
parser string
}
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
@ -167,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} else if k, v = head(opt, "="); k == "parser" {
setOpt.parser = v
}
}
@ -190,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil
}
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
switch parser {
case "encoding.TextUnmarshaler":
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
if !ok {
return false, nil
}
return true, v.UnmarshalText([]byte(val))
}
return false, nil
}
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -207,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
}
totalLength := 0
@ -230,9 +250,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() {
case reflect.Slice:
if !ok {
vs = []string{opt.defaultValue}
if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -240,7 +263,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
}
if ok, err = trySetCustom(vs[0], value); ok {
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
@ -248,11 +273,14 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, err
}
return true, setSlice(vs, value, field)
return true, setSlice(vs, value, field, opt)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -260,7 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
}
if ok, err = trySetCustom(vs[0], value); ok {
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
@ -272,27 +302,37 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
return true, setArray(vs, value, field, opt)
default:
var val string
if !ok {
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
val = opt.defaultValue
} else if len(vs) > 0 {
val = vs[0]
}
if len(vs) > 0 {
val = vs[0]
if val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field)
return true, setWithProperType(val, value, field, opt)
}
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
return err
} else if ok, err = trySetCustom(val, value); ok {
return err
}
// If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String {
val = strings.TrimSpace(val)
}
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
@ -340,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
return setWithProperType(val, value.Elem(), field, opt)
default:
return errUnknownType
}
@ -397,6 +437,11 @@ 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", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
@ -420,11 +465,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
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
@ -447,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
}
@ -457,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
}
@ -468,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
@ -489,9 +533,7 @@ func setFormMap(ptr any, form map[string][]string) error {
if !ok {
return ErrConvertMapStringSlice
}
for k, v := range form {
ptrMap[k] = v
}
maps.Copy(ptrMap, form)
return nil
}

View File

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

View File

@ -5,6 +5,7 @@
package binding
import (
"encoding"
"encoding/hex"
"errors"
"mime/multipart"
@ -226,7 +227,35 @@ func TestMappingTime(t *testing.T) {
require.Error(t, err)
}
type bindTestData struct {
need any
got any
in map[string][]string
}
func TestMappingTimeUnixNano(t *testing.T) {
type needFixUnixNanoEmpty struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
}
// ok
tests := []bindTestData{
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
}
func TestMappingTimeDuration(t *testing.T) {
type needFixDurationEmpty struct {
Duration time.Duration `form:"duration"`
}
var s struct {
D time.Duration
}
@ -236,6 +265,17 @@ func TestMappingTimeDuration(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// ok
tests := []bindTestData{
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err)
@ -485,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
assert.EqualValues(t, 245, s.Foo)
}
func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo,default=f5"`
}
err := mappingByPtr(&s, formSource{"foo": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Foo)
}
type customUnmarshalParamType struct {
Protocol string
Path string
@ -585,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) {
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, s.FileData)
}
func TestMappingCustomSliceOfSliceUri(t *testing.T) {
var s struct {
FileData []customPath `uri:"path" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
require.NoError(t, err)
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceForm(t *testing.T) {
var s struct {
FileData []customPath `form:"path" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
@ -635,3 +712,435 @@ func TestMappingCustomArrayForm(t *testing.T) {
expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayOfArrayUri(t *testing.T) {
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectID `uri:"ids" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
require.NoError(t, err)
assert.Equal(t, []objectID{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayForm(t *testing.T) {
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectID `form:"ids" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectID{id1, id2}, s.FileData)
}
// ==== TextUnmarshaler tests START ====
type customUnmarshalTextHex int
func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = customUnmarshalTextHex(v)
return nil
}
// verify type implements TextUnmarshaler
var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil)
func TestMappingCustomUnmarshalTextHexUri(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, 245, s.Field)
}
func TestMappingCustomUnmarshalTextHexForm(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 245, s.Field)
}
func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) {
var s struct {
Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Field)
}
type customUnmarshalTextType struct {
Protocol string
Path string
Name string
}
func (f *customUnmarshalTextType) UnmarshalText(text []byte) error {
parts := strings.Split(string(text), ":")
if len(parts) != 3 {
return errors.New("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
f.Name = parts[2]
return nil
}
var _ encoding.TextUnmarshaler = (*customUnmarshalTextType)(nil)
func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) {
var s struct {
FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) {
var s struct {
FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) {
var s struct {
FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) {
var s struct {
FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
type customPathUnmarshalText []string
func (p *customPathUnmarshalText) UnmarshalText(text []byte) error {
elems := strings.Split(string(text), "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil)
func TestMappingCustomSliceUnmarshalTextUri(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceUnmarshalTextForm(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) {
var s struct {
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) {
var s struct {
FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"path": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
}
type objectIDUnmarshalText [12]byte
func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error {
oid, err := convertToOidUnmarshalText(string(text))
if err != nil {
return err
}
*o = oid
return nil
}
func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) {
oid, err := convertTo(s)
return objectIDUnmarshalText(oid), err
}
var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil)
func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) {
var s struct {
FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) {
var s struct {
FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) {
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
var s struct {
FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
}
// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default
// binding logic.
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) {
var s struct {
Hex customUnmarshalParamHex `form:"hex"`
HexByUnmarshalText customUnmarshalParamHex `form:"hex2,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`f5`},
"hex2": {`f5`},
}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Hex)
assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding
}
// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the
// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards
// compatibility)
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) {
var s struct {
Hex customUnmarshalTextHex `form:"hex"`
HexByUnmarshalText customUnmarshalTextHex `form:"hex2,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`11`},
"hex2": {`11`},
}, "form")
require.NoError(t, err)
assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal
assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for normal hex binding
}
type customHexUnmarshalParamAndUnmarshalText int
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error {
return errors.New("should not be called in unit test if parser tag present")
}
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = customHexUnmarshalParamAndUnmarshalText(v)
return nil
}
// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler,
// then only the UnmarshalText method should be invoked.
func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) {
var s struct {
Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`f5`},
}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Hex)
}
// ==== TextUnmarshaler tests END ====
func TestMappingEmptyValues(t *testing.T) {
t.Run("slice with default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present but empty
err = mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present with values
err = mappingByPtr(&s, formSource{"slice": {"1", "2", "3"}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.Slice)
})
t.Run("array with default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
// field present but empty
err = mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
})
t.Run("slice without default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int(nil), s.Slice)
})
t.Run("array without default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{0}, s.Array)
})
t.Run("slice with collection format", func(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi,default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:"slice_csv,default=1;2;3" collection_format:"csv"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
// field present but empty
err = mappingByPtr(&s, formSource{"slice_multi": {}, "slice_csv": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
})
}

View File

@ -10,6 +10,7 @@ import (
"io"
"io/fs"
"log"
"maps"
"math"
"mime/multipart"
"net"
@ -38,6 +39,8 @@ const (
MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML
MIMEPROTOBUF = binding.MIMEPROTOBUF
MIMEBSON = binding.MIMEBSON
)
// BodyBytesKey indicates a default body bytes key.
@ -130,11 +133,8 @@ func (c *Context) Copy() *Context {
cp.fullPath = c.fullPath
cKeys := c.Keys
cp.Keys = make(map[any]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
@ -187,7 +187,7 @@ func (c *Context) FullPath() string {
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
for c.index < safeInt8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
@ -272,7 +272,7 @@ 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.
// 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()
@ -308,165 +308,185 @@ func getTyped[T any](c *Context, key any) (res T) {
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key any) (s string) {
func (c *Context) GetString(key any) string {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key any) (b bool) {
func (c *Context) GetBool(key any) bool {
return getTyped[bool](c, key)
}
// GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key any) (i int) {
func (c *Context) GetInt(key any) int {
return getTyped[int](c, key)
}
// GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt8(key any) (i8 int8) {
func (c *Context) GetInt8(key any) int8 {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key any) (i16 int16) {
func (c *Context) GetInt16(key any) int16 {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key any) (i32 int32) {
func (c *Context) GetInt32(key any) int32 {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key any) (i64 int64) {
func (c *Context) GetInt64(key any) int64 {
return getTyped[int64](c, key)
}
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key any) (ui uint) {
func (c *Context) GetUint(key any) uint {
return getTyped[uint](c, key)
}
// GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint8(key any) (ui8 uint8) {
func (c *Context) GetUint8(key any) uint8 {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key any) (ui16 uint16) {
func (c *Context) GetUint16(key any) uint16 {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key any) (ui32 uint32) {
func (c *Context) GetUint32(key any) uint32 {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key any) (ui64 uint64) {
func (c *Context) GetUint64(key any) uint64 {
return getTyped[uint64](c, key)
}
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key any) (f32 float32) {
func (c *Context) GetFloat32(key any) float32 {
return getTyped[float32](c, key)
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key any) (f64 float64) {
func (c *Context) GetFloat64(key any) float64 {
return getTyped[float64](c, key)
}
// GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key any) (t time.Time) {
func (c *Context) GetTime(key any) time.Time {
return getTyped[time.Time](c, key)
}
// GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key any) (d time.Duration) {
func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key)
}
// GetError returns the value associated with the key as an error.
func (c *Context) GetError(key any) error {
return getTyped[error](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) (is []int) {
func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key any) (i8s []int8) {
func (c *Context) GetInt8Slice(key any) []int8 {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key any) (i16s []int16) {
func (c *Context) GetInt16Slice(key any) []int16 {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key any) (i32s []int32) {
func (c *Context) GetInt32Slice(key any) []int32 {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key any) (i64s []int64) {
func (c *Context) GetInt64Slice(key any) []int64 {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key any) (uis []uint) {
func (c *Context) GetUintSlice(key any) []uint {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key any) (ui8s []uint8) {
func (c *Context) GetUint8Slice(key any) []uint8 {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key any) (ui16s []uint16) {
func (c *Context) GetUint16Slice(key any) []uint16 {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key any) (ui32s []uint32) {
func (c *Context) GetUint32Slice(key any) []uint32 {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key any) (ui64s []uint64) {
func (c *Context) GetUint64Slice(key any) []uint64 {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key any) (f32s []float32) {
func (c *Context) GetFloat32Slice(key any) []float32 {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key any) (f64s []float64) {
func (c *Context) GetFloat64Slice(key any) []float64 {
return getTyped[[]float64](c, key)
}
// GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key any) (ss []string) {
func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key)
}
// GetErrorSlice returns the value associated with the key as a slice of errors.
func (c *Context) GetErrorSlice(key any) []error {
return getTyped[[]error](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key any) (sm map[string]any) {
func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key)
}
// GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key any) (sms map[string]string) {
func (c *Context) GetStringMapString(key any) map[string]string {
return getTyped[map[string]string](c, key)
}
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key any) (smss map[string][]string) {
func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
return getTyped[map[string][]string](c, key)
}
// Delete deletes the key from the Context's Key map, if it exists.
// This operation is safe to be used by concurrent go-routines
func (c *Context) Delete(key any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys != nil {
delete(c.Keys, key)
}
}
/************************************/
/************ INPUT DATA ************/
/************************************/
@ -731,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
// "application/json" --> JSON binding
// "application/xml" --> XML binding
//
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
// It decodes the payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
@ -812,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())
@ -821,41 +841,71 @@ func (c *Context) ShouldBind(obj any) error {
}
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
//
// Example:
//
// POST /user
// Content-Type: application/json
//
// Request Body:
// {
// "name": "Manu",
// "age": 20
// }
//
// type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// var user User
// if err := c.ShouldBindJSON(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
// }
// c.JSON(http.StatusOK, user)
func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
// It works like ShouldBindJSON but binds query parameters from the URL.
func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// It works like ShouldBindJSON but binds plain text data from the request body.
func (c *Context) ShouldBindPlain(obj any) error {
return c.ShouldBindWith(obj, binding.Plain)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
// It works like ShouldBindJSON but binds values from HTTP headers.
func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
// It works like ShouldBindJSON but binds parameters from the URI.
func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params))
for _, v := range c.Params {
@ -939,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
}
@ -992,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
@ -1159,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})
@ -1174,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})
@ -1272,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.
@ -1305,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
}

View File

@ -32,6 +32,7 @@ import (
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
@ -292,7 +293,7 @@ func TestContextReset(t *testing.T) {
assert.Empty(t, c.Errors.Errors())
assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
assert.Empty(t, c.Params)
assert.EqualValues(t, c.index, -1)
assert.EqualValues(t, -1, c.index)
assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
}
@ -384,7 +385,7 @@ func TestContextSetGetValues(t *testing.T) {
c.Set("intInterface", a)
assert.Exactly(t, "this is a string", c.MustGet("string").(string))
assert.Exactly(t, c.MustGet("int32").(int32), int32(-42))
assert.Exactly(t, int32(-42), c.MustGet("int32").(int32))
assert.Exactly(t, int64(42424242424242), c.MustGet("int64").(int64))
assert.Exactly(t, uint64(42), c.MustGet("uint64").(uint64))
assert.InDelta(t, float32(4.2), c.MustGet("float32").(float32), 0.01)
@ -404,6 +405,19 @@ func TestContextSetGetBool(t *testing.T) {
assert.True(t, c.GetBool("bool"))
}
func TestSetGetDelete(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "example-key"
value := "example-value"
c.Set(key, value)
val, exists := c.Get(key)
assert.True(t, exists)
assert.Equal(t, val, value)
c.Delete(key)
_, exists = c.Get(key)
assert.False(t, exists)
}
func TestContextGetInt(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("int", 1)
@ -503,6 +517,14 @@ func TestContextGetDuration(t *testing.T) {
assert.Equal(t, time.Second, c.GetDuration("duration"))
}
func TestContextGetError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error"
value := errors.New("test error")
c.Set(key, value)
assert.Equal(t, value, c.GetError(key))
}
func TestContextGetIntSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "int-slice"
@ -605,6 +627,14 @@ func TestContextGetStringSlice(t *testing.T) {
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
}
func TestContextGetErrorSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error-slice"
value := []error{errors.New("error1"), errors.New("error2")}
c.Set(key, value)
assert.Equal(t, value, c.GetErrorSlice(key))
}
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
m := make(map[string]any)
@ -1001,6 +1031,7 @@ func TestContextGetCookie(t *testing.T) {
}
func TestContextBodyAllowedForStatus(t *testing.T) {
assert.False(t, bodyAllowedForStatus(http.StatusContinue))
assert.False(t, bodyAllowedForStatus(http.StatusProcessing))
assert.False(t, bodyAllowedForStatus(http.StatusNoContent))
assert.False(t, bodyAllowedForStatus(http.StatusNotModified))
@ -1130,6 +1161,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
// Multiple X-Forwarded-For headers
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 5.6.7.8 (last non-trusted IP)
assert.Equal(t, "5.6.7.8", c.ClientIP())
}
func TestContextClientIPWithSingleHeader(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 1.2.3.4
assert.Equal(t, "1.2.3.4", c.ClientIP())
}
// Tests that the response is serialized as Secure JSON
// and Content-Type is set to application/json
func TestContextRenderSecureJSON(t *testing.T) {
@ -1233,7 +1295,7 @@ func TestContextRenderNoContentHTML(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextXML tests that the response is serialized as XML
// TestContextRenderXML tests that the response is serialized as XML
// and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) {
w := httptest.NewRecorder()
@ -1258,6 +1320,33 @@ func TestContextRenderNoContentXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderPDF tests that the response is serialized as PDF
// and Content-Type is set to application/pdf
func TestContextRenderPDF(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
data := []byte("%Test pdf content")
c.PDF(http.StatusCreated, data)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, data, w.Body.Bytes())
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
}
// Tests that no PDF is rendered if code is 204
func TestContextRenderNoContentPDF(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
data := []byte("%Test pdf content")
c.PDF(http.StatusNoContent, data)
assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.Bytes())
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
}
// TestContextRenderString tests that the response is returned
// with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) {
@ -1615,6 +1704,49 @@ func TestContextNegotiationWithHTML(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithPROTOBUF(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}
c.Negotiate(http.StatusCreated, Negotiate{
Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML},
Data: data,
})
// Marshal original data for comparison
protoData, err := proto.Marshal(data)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithBSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
Data: H{"foo": "bar"},
})
bData, _ := bson.Marshal(H{"foo": "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(bData), w.Body.String())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1845,6 +1977,16 @@ func TestContextClientIP(t *testing.T) {
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
// unix address
addr := &net.UnixAddr{Net: "unix", Name: "@"}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr))
c.Request.RemoteAddr = addr.String()
assert.Equal(t, "20.20.20.20", c.ClientIP())
// reset
c.Request = c.Request.WithContext(context.Background())
resetContextForClientIPTests(c)
// Legacy tests (validating that the defaults don't break the
// (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP())
@ -1871,7 +2013,7 @@ func TestContextClientIP(t *testing.T) {
resetContextForClientIPTests(c)
// IPv6 support
c.Request.RemoteAddr = "[::1]:12345"
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
assert.Equal(t, "20.20.20.20", c.ClientIP())
resetContextForClientIPTests(c)
@ -2833,6 +2975,16 @@ func TestContextGetRawData(t *testing.T) {
assert.Equal(t, "Fetch binary post data", string(data))
}
func TestContextGetRawDataNilBody(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodPost, "/", nil)
data, err := c.GetRawData()
assert.Nil(t, data)
require.Error(t, err)
assert.Equal(t, "cannot read nil body", err.Error())
}
func TestContextRenderDataFromReader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -3173,7 +3325,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
}()
addr := strings.Split(l.Addr().String(), ":")
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
if err != nil {
t.Error(fmt.Errorf("request error: %w", err))
return
@ -3320,7 +3472,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Max-Age=1")
assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax")
// Test that when Path is empty, "/" is automatically set
@ -3341,7 +3493,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Max-Age=1")
assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax")
// Test additional cookie attributes (Expires)
@ -3364,7 +3516,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Domain=localhost")
assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax")
// Test for Partitioned attribute (Go 1.18+)
@ -3384,7 +3536,7 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "Domain=localhost")
assert.Contains(t, setCookie, "HttpOnly")
assert.Contains(t, setCookie, "Secure")
// SameSite=Lax might be omitted in Go 1.23+ as it's the default
// SameSite=Lax might be omitted in Go 1.24+ as it's the default
// assert.Contains(t, setCookie, "SameSite=Lax")
// Not testing for Partitioned attribute as it may not be supported in all Go versions
@ -3421,6 +3573,24 @@ func TestContextSetCookieData(t *testing.T) {
setCookie := c.Writer.Header().Get("Set-Cookie")
assert.Contains(t, setCookie, "SameSite=None")
})
// Test that SameSiteDefaultMode inherits from context's sameSite
t.Run("SameSiteDefaultMode inherits context sameSite", func(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetSameSite(http.SameSiteStrictMode)
cookie := &http.Cookie{
Name: "user",
Value: "gin",
Path: "/",
Domain: "localhost",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteDefaultMode,
}
c.SetCookieData(cookie)
setCookie := c.Writer.Header().Get("Set-Cookie")
assert.Contains(t, setCookie, "SameSite=Strict")
})
}
func TestGetMapFromFormData(t *testing.T) {
@ -3581,22 +3751,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
// Test case 3: Large dataset with many bracket keys
largeData := make(map[string][]string)
for i := 0; i < 100; i++ {
for i := range 100 {
key := fmt.Sprintf("ids[%d]", i)
largeData[key] = []string{fmt.Sprintf("value%d", i)}
}
for i := 0; i < 50; i++ {
for i := range 50 {
key := fmt.Sprintf("names[%d]", i)
largeData[key] = []string{fmt.Sprintf("name%d", i)}
}
for i := 0; i < 25; i++ {
for i := range 25 {
key := fmt.Sprintf("other[key%d]", i)
largeData[key] = []string{fmt.Sprintf("other%d", i)}
}
// Test case 4: Dataset with many non-matching keys (worst case)
worstCaseData := make(map[string][]string)
for i := 0; i < 100; i++ {
for i := range 100 {
key := fmt.Sprintf("nonmatching%d", i)
worstCaseData[key] = []string{fmt.Sprintf("value%d", i)}
}
@ -3632,7 +3802,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_, _ = getMapFromFormData(bm.data, bm.key)
}
})

View File

@ -13,7 +13,9 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 23
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.
@ -77,8 +79,8 @@ func getMinVer(v string) (uint64, error) {
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.25+.
`)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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.

76
gin.go
View File

@ -11,7 +11,6 @@ import (
"net/http"
"os"
"path"
"regexp"
"strings"
"sync"
@ -23,10 +22,12 @@ import (
"golang.org/x/net/http2/h2c"
)
const defaultMultipartMemory = 32 << 20 // 32 MB
const escapedColon = "\\:"
const colon = ":"
const backslash = "\\"
const (
defaultMultipartMemory = 32 << 20 // 32 MB
escapedColon = "\\:"
colon = ":"
backslash = "\\"
)
var (
default404Body = []byte("404 page not found")
@ -46,9 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
},
}
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@ -94,6 +92,10 @@ const (
type Engine struct {
RouterGroup
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
routeTreesUpdated sync.Once
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
@ -133,10 +135,16 @@ type Engine struct {
AppEngine bool
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
// Only use RawPath if you know what you are doing.
UseRawPath bool
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
// It overrides UseRawPath
UseEscapedPath bool
// UnescapePathValues if true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
@ -189,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UseEscapedPath: false
// - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
@ -206,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
UseEscapedPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
@ -537,7 +547,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return
}
@ -553,7 +567,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
server := &http.Server{ // #nosec G112
Addr: addr,
Handler: engine.Handler(),
}
err = server.ListenAndServeTLS(certFile, keyFile)
return
}
@ -576,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
}
@ -593,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
@ -629,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
@ -662,7 +691,11 @@ 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
}
@ -749,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
}
@ -761,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
redirectRequest(c)
}
// sanitizePathChars removes unsafe characters from path strings,
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
func sanitizePathChars(s string) string {
return strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
return r
}
return -1
}, s)
}
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
rPath := req.URL.Path

View File

@ -12,17 +12,9 @@ import (
"github.com/gin-gonic/gin"
)
var (
once sync.Once
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) {

246
ginS/gins_test.go Normal file
View File

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

View File

@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
@ -69,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:8080/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example")
@ -212,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:3123/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example")
@ -233,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run(":5150"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:5150/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example")
@ -261,10 +265,11 @@ func TestUnixSocket(t *testing.T) {
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -322,10 +327,11 @@ func TestFileDescriptor(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -354,10 +360,11 @@ func TestListener(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
var responseBuilder strings.Builder
for scanner.Scan() {
response += scanner.Text()
responseBuilder.WriteString(scanner.Text())
}
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@ -393,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) {
var wg sync.WaitGroup
iterations := 200
wg.Add(iterations)
for i := 0; i < iterations; i++ {
for range iterations {
go func() {
req, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)

View File

@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
}
func TestH2c(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
ln, err := net.Listen("tcp", localhostIP+":0")
if err != nil {
t.Error(err)
}
@ -545,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) {
@ -720,6 +743,127 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
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()
@ -913,3 +1057,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
func TestLiteralColonWithRun(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.updateRouteTrees()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHandler(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
handler := router.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHTTPServer(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.GET("/test/:param", func(c *Context) {
c.JSON(http.StatusOK, H{"param": c.Param("param")})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Contains(t, w2.Body.String(), "foo")
}
// Test that updateRouteTrees is called only once
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.String(http.StatusOK, "ok")
})
for range 5 {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ok", w.Body.String())
}
}

45
go.mod
View File

@ -1,44 +1,45 @@
module github.com/gin-gonic/gin
go 1.23.0
go 1.25.0
require (
github.com/bytedance/sonic v1.14.0
github.com/bytedance/sonic v1.15.0
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.27.0
github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.18.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/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.54.0
github.com/quic-go/quic-go v0.59.0
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.0
golang.org/x/net v0.42.0
google.golang.org/protobuf v1.36.9
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.3.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.8 // 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.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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
)

91
go.sum
View File

@ -1,14 +1,17 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
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=
@ -17,12 +20,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.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=
@ -30,58 +33,64 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
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.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.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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
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=

View File

@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte {
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
for range 100 {
_, err := cRand.Read(data)
if err != nil {
t.Fatal(err)
@ -41,6 +41,15 @@ func TestBytesToString(t *testing.T) {
}
}
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
@ -70,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")
@ -78,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)
}
}

View File

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

View File

@ -39,7 +39,7 @@ func TestLogger(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
// than individual functions. I'm not sure where these should go.
buffer.Reset()
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
@ -103,7 +103,7 @@ func TestLoggerWithConfig(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
// than individual functions. I'm not sure where these should go.
buffer.Reset()
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
@ -277,11 +277,11 @@ func TestDefaultLogFormatter(t *testing.T) {
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) {
@ -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())
@ -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")
}

55
path.go
View File

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

View File

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

View File

@ -5,25 +5,28 @@
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"
)
const dunno = "???"
var dunnoBytes = []byte(dunno)
const (
dunno = "???"
stackSkip = 3
)
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any)
@ -54,38 +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 {
const stackSkip = 3
if brokenPipe {
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
if isBrokenPipe {
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack(stackSkip), reset)
timeFormat(time.Now()), rec, stack(stackSkip), reset)
}
}
if brokenPipe {
if isBrokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck
c.Error(err) //nolint: errcheck
c.Abort()
} else {
handle(c, err)
handle(c, rec)
}
}
}()
@ -117,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 {
@ -127,25 +128,44 @@ 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 dunnoBytes
// 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.

View File

@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery",
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
router := New()
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, dunnoBytes, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, dunnoBytes, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
}
func TestFunction(t *testing.T) {
bs := function(1)
assert.Equal(t, dunno, bs)
@ -113,13 +98,13 @@ func TestFunction(t *testing.T) {
func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204
expectMsgs := map[syscall.Errno]string{
syscall.EPIPE: "broken pipe",
syscall.ECONNRESET: "connection reset by peer",
expectErrnos := []syscall.Errno{
syscall.EPIPE,
syscall.ECONNRESET,
}
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
for _, errno := range expectErrnos {
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
var buf strings.Builder
router := New()
@ -137,11 +122,36 @@ func TestPanicWithBrokenPipe(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
})
}
}
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
func TestPanicWithAbortHandler(t *testing.T) {
const expectCode = 204
var buf strings.Builder
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Panic with ErrAbortHandler which should be treated as broken pipe
panic(http.ErrAbortHandler)
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
out := buf.String()
assert.Contains(t, out, "net/http: abort Handler")
assert.NotContains(t, out, "panic recovered")
}
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)
@ -152,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
}
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -171,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
@ -187,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
}
router.Use(CustomRecovery(handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -206,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
@ -222,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
}
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
router.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
panic("Oops, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
assert.Contains(t, buffer.String(), t.Name())
assert.NotContains(t, buffer.String(), "GET /recovery")
@ -241,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
SetMode(TestMode)
}
@ -307,3 +317,53 @@ func TestSecureRequestDump(t *testing.T) {
})
}
}
// TestReadNthLine tests the readNthLine function with various scenarios.
func TestReadNthLine(t *testing.T) {
// Create a temporary test file
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
tempFile, err := os.CreateTemp("", "testfile*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tempFile.Name())
// Write test content to the temporary file
if _, err := tempFile.WriteString(testContent); err != nil {
t.Fatal(err)
}
if err := tempFile.Close(); err != nil {
t.Fatal(err)
}
// Test cases
tests := []struct {
name string
lineNum int
fileName string
want string
wantErr bool
}{
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := readNthLine(tt.fileName, tt.lineNum)
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, tt.want, got)
})
}
}
func BenchmarkStack(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = stack(stackSkip)
}
}

34
render/bson.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
// BSON contains the given interface object.
type BSON struct {
Data any
}
var bsonContentType = []string{"application/bson"}
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
func (r BSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := bson.Marshal(&r.Data)
if err == nil {
_, err = w.Write(bytes)
}
return err
}
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
func (r BSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, bsonContentType)
}

View File

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

26
render/pdf.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2026 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import "net/http"
// PDF contains the given PDF binary data.
type PDF struct {
Data []byte
}
var pdfContentType = []string{"application/pdf"}
// Render (PDF) writes PDF data with custom ContentType.
func (r PDF) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
_, err := w.Write(r.Data)
return err
}
// WriteContentType (PDF) writes PDF ContentType for response.
func (r PDF) WriteContentType(w http.ResponseWriter) {
writeContentType(w, pdfContentType)
}

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"encoding/xml"
"errors"
"html/template"
"io"
"net"
"net/http"
"net/http/httptest"
@ -15,16 +16,13 @@ import (
"strings"
"testing"
"github.com/gin-gonic/gin/codec/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
// TODO unit tests
// test errors
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
@ -139,19 +137,44 @@ func TestRenderJsonpJSON(t *testing.T) {
}
type errorWriter struct {
bufString string
bufString string
ErrThreshold int // 1-based threshold. If 1, errors on the 1st Write call.
writeCount int
*httptest.ResponseRecorder
}
var _ http.ResponseWriter = (*errorWriter)(nil)
func (w *errorWriter) Header() http.Header {
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
return w.ResponseRecorder.Header()
}
func (w *errorWriter) WriteHeader(statusCode int) {
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
w.ResponseRecorder.WriteHeader(statusCode)
}
func (w *errorWriter) Write(buf []byte) (int, error) {
if string(buf) == w.bufString {
return 0, errors.New(`write "` + w.bufString + `" error`)
w.writeCount++
if (w.bufString != "" && string(buf) == w.bufString) || (w.ErrThreshold > 0 && w.writeCount >= w.ErrThreshold) {
return 0, errors.New(`write error`)
}
if w.ResponseRecorder == nil {
w.ResponseRecorder = httptest.NewRecorder()
}
return w.ResponseRecorder.Write(buf)
}
func (w *errorWriter) reset() {
w.writeCount = 0
w.ResponseRecorder = httptest.NewRecorder()
}
func TestRenderJsonpJSONError(t *testing.T) {
ew := &errorWriter{
ResponseRecorder: httptest.NewRecorder(),
@ -164,23 +187,33 @@ func TestRenderJsonpJSONError(t *testing.T) {
},
}
cb := template.JSEscapeString(jsonpJSON.Callback)
ew.bufString = cb
err := jsonpJSON.Render(ew) // error was returned while writing callback
assert.Equal(t, `write "`+cb+`" error`, err.Error())
// error was returned while writing callback
ew.reset()
ew.ErrThreshold = 1
err := jsonpJSON.Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
ew.bufString = `(`
// error was returned while writing "("
ew.reset()
ew.ErrThreshold = 2
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`(`+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
ew.bufString = string(data)
// error was returned while writing data
ew.reset()
ew.ErrThreshold = 3
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
ew.bufString = `);`
// error was returned while writing ");"
ew.reset()
ew.ErrThreshold = 4
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`);`+`" error`, err.Error())
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderJsonpJSONError2(t *testing.T) {
@ -359,6 +392,55 @@ func TestRenderProtoBufFail(t *testing.T) {
require.Error(t, err)
}
func TestRenderBSON(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
type mystruct struct {
Label string
Reps []int64
}
data := &mystruct{
Label: "test",
Reps: reps,
}
(BSON{data}).WriteContentType(w)
bsonData, err := bson.Marshal(data)
require.NoError(t, err)
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
err = (BSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, bsonData, w.Body.Bytes())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestRenderBSONError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
err := (BSON{data}).Render(w)
require.Error(t, err)
}
func TestRenderBSONWriteError(t *testing.T) {
type testStruct struct {
Value string
}
data := &testStruct{Value: "test"}
ew := &errorWriter{
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
err := (BSON{data}).Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
data := xmlmap{
@ -375,6 +457,31 @@ func TestRenderXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderXMLError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
err := (XML{data}).Render(w)
require.Error(t, err)
assert.Contains(t, err.Error(), "xml: unsupported type: chan int")
}
func TestRenderPDF(t *testing.T) {
w := httptest.NewRecorder()
data := []byte("%Test pdf content")
pdf := PDF{data}
pdf.WriteContentType(w)
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
err := pdf.Render(w)
require.NoError(t, err)
assert.Equal(t, data, w.Body.Bytes())
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
}
func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
require.NoError(t, err)
@ -427,6 +534,52 @@ func TestRenderData(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, "19", w.Header().Get("Content-Length"))
}
func TestRenderDataContentLength(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size, err := strconv.Atoi(r.URL.Query().Get("size"))
assert.NoError(t, err)
data := Data{
ContentType: "application/octet-stream",
Data: make([]byte, size),
}
assert.NoError(t, data.Render(w))
}))
t.Cleanup(srv.Close)
for _, size := range []int{0, 1, 100, 100_000} {
t.Run(strconv.Itoa(size), func(t *testing.T) {
resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type"))
assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length"))
actual, err := io.Copy(io.Discard, resp.Body)
require.NoError(t, err)
assert.EqualValues(t, size, actual)
})
}
}
func TestRenderDataError(t *testing.T) {
ew := &errorWriter{
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
data := []byte("#!PNG some raw data")
err := (Data{
ContentType: "image/png",
Data: data,
}).Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderString(t *testing.T) {
@ -568,6 +721,32 @@ func TestRenderHTMLDebugPanics(t *testing.T) {
assert.Panics(t, func() { htmlRender.Instance("", nil) })
}
func TestRenderHTMLTemplateError(t *testing.T) {
w := httptest.NewRecorder()
templ := template.Must(template.New("t").Parse(`Hello {{if .name}}{{.name.DoesNotExist}}{{end}}`))
htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("t", map[string]any{
"name": "alexandernyquist",
})
err := instance.Render(w)
require.Error(t, err)
}
func TestRenderHTMLTemplateExecuteError(t *testing.T) {
w := httptest.NewRecorder()
templ := template.Must(template.New("t").Parse(`Hello {{.name.invalid}}`))
htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("t", map[string]any{
"name": "alexandernyquist",
})
err := instance.Render(w)
require.Error(t, err)
}
func TestRenderReader(t *testing.T) {
w := httptest.NewRecorder()
@ -619,10 +798,10 @@ func TestRenderWriteError(t *testing.T) {
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
bufString: prefix,
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
err := r.Render(ew)
require.Error(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error())
assert.Equal(t, "write error", err.Error())
}

View File

@ -17,7 +17,7 @@ const (
defaultStatus = http.StatusOK
)
var errHijackAlreadyWritten = errors.New("gin: response already written")
var errHijackAlreadyWritten = errors.New("gin: response body already written")
// ResponseWriter ...
type ResponseWriter interface {
@ -109,7 +109,9 @@ func (w *responseWriter) Written() bool {
// Hijack implements the http.Hijacker interface.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.Written() {
// 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 {
@ -126,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flusher interface.
func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {

View File

@ -194,6 +194,64 @@ func TestResponseWriterHijackAfterWrite(t *testing.T) {
}
}
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
action: func(w ResponseWriter) error {
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
return nil
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
expectWrittenAfterHijack: true,
expectError: nil,
},
{
name: "hijack after WriteHeaderNow + Write should fail",
action: func(w ResponseWriter) error {
w.WriteHeaderNow()
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
_, _, hijackErr := w.Hijack()
if tc.expectError == nil {
require.NoError(t, hijackErr, "expected hijack to succeed")
} else {
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
}
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{}

View File

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

View File

@ -4,7 +4,11 @@
package gin
import "net/http"
import (
"fmt"
"net/http"
"time"
)
// CreateTestContext returns a fresh Engine and a Context associated with it.
// This is useful for tests that need to set up a new Gin engine instance
@ -29,3 +33,25 @@ func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c.writermem.reset(w)
return
}
// waitForServerReady waits for a server to be ready by making HTTP requests
// with exponential backoff. This is more reliable than time.Sleep() for testing.
func waitForServerReady(url string, maxAttempts int) error {
client := &http.Client{
Timeout: 100 * time.Millisecond,
}
for i := 0; i < maxAttempts; i++ {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
return nil
}
// Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms...
backoff := min(time.Duration(10*(1<<uint(i)))*time.Millisecond, 500*time.Millisecond)
time.Sleep(backoff)
}
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
}

92
tree.go
View File

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

View File

@ -1018,3 +1018,96 @@ func TestWildcardInvalidSlash(t *testing.T) {
}
}
}
func TestTreeFindCaseInsensitivePathWithMultipleChildrenAndWildcard(t *testing.T) {
tree := &node{}
// Setup routes that create a node with both static children and a wildcard child.
// This configuration previously caused a panic ("invalid node type") in
// findCaseInsensitivePathRec because it accessed children[0] instead of the
// wildcard child (which is always at the end of the children array).
// See: https://github.com/gin-gonic/gin/issues/2959
routes := [...]string{
"/aa/aa",
"/:bb/aa",
}
for _, route := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv != nil {
t.Fatalf("panic inserting route '%s': %v", route, recv)
}
}
// These lookups previously panicked with "invalid node type" because
// findCaseInsensitivePathRec picked children[0] (a static node) instead
// of the wildcard child at the end of the array.
out, found := tree.findCaseInsensitivePath("/aa", true)
if found {
t.Errorf("Expected no match for '/aa', but got: %s", string(out))
}
out, found = tree.findCaseInsensitivePath("/aa/aa/aa/aa", true)
if found {
t.Errorf("Expected no match for '/aa/aa/aa/aa', but got: %s", string(out))
}
// Case-insensitive lookup should match the static route /aa/aa
out, found = tree.findCaseInsensitivePath("/AA/AA", true)
if !found {
t.Error("Route '/AA/AA' not found via case-insensitive lookup")
} else if string(out) != "/aa/aa" {
t.Errorf("Wrong result for '/AA/AA': expected '/aa/aa', got: %s", string(out))
}
}
func TestTreeFindCaseInsensitivePathWildcardParamAndStaticChild(t *testing.T) {
tree := &node{}
// Another variant: param route + static route under same prefix
routes := [...]string{
"/prefix/:id",
"/prefix/xxx",
}
for _, route := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv != nil {
t.Fatalf("panic inserting route '%s': %v", route, recv)
}
}
// Should NOT panic even for paths that don't match any route
out, found := tree.findCaseInsensitivePath("/prefix/a/b/c", true)
if found {
t.Errorf("Expected no match for '/prefix/a/b/c', but got: %s", string(out))
}
// Exact match should still work
out, found = tree.findCaseInsensitivePath("/prefix/xxx", true)
if !found {
t.Error("Route '/prefix/xxx' not found")
} else if string(out) != "/prefix/xxx" {
t.Errorf("Wrong result for '/prefix/xxx': %s", string(out))
}
// Case-insensitive match should work
out, found = tree.findCaseInsensitivePath("/PREFIX/XXX", true)
if !found {
t.Error("Route '/PREFIX/XXX' not found via case-insensitive lookup")
} else if string(out) != "/prefix/xxx" {
t.Errorf("Wrong result for '/PREFIX/XXX': expected '/prefix/xxx', got: %s", string(out))
}
// Param route should still match
out, found = tree.findCaseInsensitivePath("/prefix/something", true)
if !found {
t.Error("Route '/prefix/something' not found via param match")
} else if string(out) != "/prefix/something" {
t.Errorf("Wrong result for '/prefix/something': %s", string(out))
}
}

View File

@ -6,6 +6,7 @@ package gin
import (
"encoding/xml"
"math"
"net/http"
"os"
"path"
@ -18,6 +19,12 @@ import (
// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey"
// localhostIP indicates the default localhost IP address.
const localhostIP = "127.0.0.1"
// localhostIPv6 indicates the default localhost IPv6 address.
const localhostIPv6 = "::1"
// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val any) HandlerFunc {
value := reflect.ValueOf(val)
@ -155,10 +162,26 @@ func resolveAddress(addr []string) string {
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
for i := range len(s) {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
func safeInt8(n int) int8 {
if n > math.MaxInt8 {
return math.MaxInt8
}
return int8(n)
}
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
func safeUint16(n int) uint16 {
if n > math.MaxUint16 {
return math.MaxUint16
}
return uint16(n)
}

View File

@ -8,10 +8,12 @@ import (
"bytes"
"encoding/xml"
"fmt"
"math"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func init() {
@ -19,7 +21,7 @@ func init() {
}
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}
@ -144,7 +146,28 @@ func TestMarshalXMLforH(t *testing.T) {
assert.Error(t, e)
}
func TestMarshalXMLforHSuccess(t *testing.T) {
h := H{
"key1": "value1",
"key2": 123,
}
data, err := xml.Marshal(h)
require.NoError(t, err)
assert.Contains(t, string(data), "<key1>value1</key1>")
assert.Contains(t, string(data), "<key2>123</key2>")
}
func TestIsASCII(t *testing.T) {
assert.True(t, isASCII("test"))
assert.False(t, isASCII("🧡💛💚💙💜"))
}
func TestSafeInt8(t *testing.T) {
assert.Equal(t, int8(100), safeInt8(100))
assert.Equal(t, int8(math.MaxInt8), safeInt8(int(math.MaxInt8)+123))
}
func TestSafeUint16(t *testing.T) {
assert.Equal(t, uint16(100), safeUint16(100))
assert.Equal(t, uint16(math.MaxUint16), safeUint16(int(math.MaxUint16)+123))
}

View File

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