Compare commits

..

186 Commits

Author SHA1 Message Date
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
Bo-Yi Wu
6ad6205e9c
docs(changelog): upgrade Gin to v1.11.0 and add release notes (#4361)
- Add release notes for Gin v1.11.0, detailing new features, enhancements, bug fixes, CI/build improvements, dependency updates, and documentation changes
- Update Gin framework version to v1.11.0

ref: https://github.com/gin-gonic/gin/issues/4325

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-20 19:38:42 +08:00
Bo-Yi Wu
7858527c8c
docs(changelog): update release notes for Gin v1.10.1 (#4360)
- Add release notes for Gin v1.10.1, including new features, enhancements, and build process updates

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-20 19:32:43 +08:00
Bo-Yi Wu
cb000f570c
ci: integrate Trivy vulnerability scanning into CI workflow (#4359)
- Add a GitHub Actions job for vulnerability scanning using Trivy
- Configure Trivy to scan the repository for vulnerabilities of severity critical, high, and medium
- Ensure the workflow fails if vulnerabilities are found

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-09-20 19:24:57 +08:00
Flc゛
2119046230
ci: support Go 1.25 (#4341)
- Update GitHub Actions workflow to include Go 1.25 in the test matrix
- This change expands the range of Go versions tested for the project

Signed-off-by: Flc <four_leaf_clover@foxmail.com>
2025-09-19 09:44:22 +08:00
Flc゛
da372fc778
build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 (#4342)
* build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* test: update expected status code for request too large test

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

---------

Signed-off-by: Flc <four_leaf_clover@foxmail.com>
2025-09-19 08:40:33 +08:00
Name
e198f6e859
refactor(render): remove headers parameter from writeHeader (#4353)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-09-19 08:39:17 +08:00
dependabot[bot]
cca98d2d26
chore(deps): bump google.golang.org/protobuf from 1.36.8 to 1.36.9 (#4356)
Bumps google.golang.org/protobuf from 1.36.8 to 1.36.9.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.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>
2025-09-19 08:35:54 +08:00
Name
9b1e3533e2
refactor(tree): replace string(/) with "/" in node.insertChild (#4354)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-09-19 08:35:34 +08:00
Name
f9bd00a6b7
perf(context): optimize getMapFromFormData performance (#4339)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-09-14 07:29:11 +08:00
dependabot[bot]
28172fa682
chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.8 (#4346)
Bumps google.golang.org/protobuf from 1.36.6 to 1.36.8.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.8
  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-14 07:26:29 +08:00
dependabot[bot]
46150257b3
chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 (#4347)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.10.0 to 1.11.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.11.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>
2025-09-14 07:26:06 +08:00
dependabot[bot]
e7693e67c2
chore(deps): bump actions/setup-go from 5 to 6 (#4351)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  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>
2025-09-14 07:25:36 +08:00
dependabot[bot]
077a2f39c8
chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 (#4328)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.53.0 to 0.54.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.54.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-08-06 22:16:53 +08:00
Name
45b805f6d5
perf(recovery): optimize the log output of CustomRecoveryWithWriter (#4258)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-08-02 12:30:14 +08:00
Varus Hsu
17d0b553ea
chore(render): do not export tomlContentType anymore (#4319) 2025-08-02 12:27:59 +08:00
諏訪原慶斗
42f93283cf
docs(test): improved GoDoc in test_helpers.go (#4270) 2025-08-02 12:23:20 +08:00
諏訪原慶斗
32065bbd42
chore(response): prevention of Hijack() runtime panics (#4295)
* Prevention of Hijack() runtime panics

* added test of Hijack()

* fix review

* fix lint error

* added check assertion of Wrrten() condition before calling Hijack()
2025-08-02 12:16:58 +08:00
Denny Septian Panggabean
b987b6206f
build: make automatically update package in golang (#4311) 2025-07-26 21:02:59 +08:00
Leon cap
dab5944a7b
test(context): add comprehensive unit tests for Context.File() method (#4307)
* test: add comprehensive unit tests for Context.File() method

- Add TestContextFile with multiple test scenarios in context_test.go
- Add simplified tests in context_file_test.go
- Cover file serving, 404 handling, directory access, HEAD requests, and Range requests
- All tests pass successfully

* fix: resolve gci formatting issues in test files

* fix: correct test file paths and add test file to gin directory

* move test_file.txt to testdata directory as suggested by maintainer

* fix: update test file paths to use testdata directory
2025-07-22 21:38:32 +08:00
Name
9708475b3b
docs(context): fix AbortWithStatusPureJSON comment typo (#4310)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-07-22 21:36:47 +08:00
Name
e4c2a27624
refactor(context): remove unused Context dependency in get method (#4304)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-07-22 11:19:08 +08:00
chenhuiluo
a4ac275e07
test(route): add some test for routergroup (#4291)
Co-authored-by: chenhuiluo <luochenhui@butterfly.tech>
2025-07-19 15:49:41 +08:00
dependabot[bot]
ae5be7fcb7
chore(deps): bump golang.org/x/net from 0.41.0 to 0.42.0 (#4297)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.41.0 to 0.42.0.
- [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.42.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-07-19 15:08:22 +08:00
maskpp
57ec9e6036
chore(mode): remove impossible case (empty value for mode) (#4303) 2025-07-19 15:07:44 +08:00
Leon cap
ad4f436ae9
docs: correct article usage in comments (#4301)
- Fix 'an url' to 'a URL' in logger.go comment
- Fix 'an form' to 'a form' in binding/form_mapping.go comment
- Improve grammar consistency in code documentation
2025-07-19 14:58:12 +08:00
諏訪原慶斗
5826722a87
fix: version number discrepancy (#4299) 2025-07-17 19:51:40 +08:00
諏訪原慶斗
bdc1ad7987
docs: added comment in doc.go (#4274) 2025-07-13 09:43:32 +08:00
dependabot[bot]
545fd74379
chore(deps): bump github.com/go-playground/validator/v10 (#4289)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.26.0 to 10.27.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.26.0...v10.27.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.27.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-07-13 09:41:58 +08:00
dependabot[bot]
a6287825c9
chore(deps): bump github.com/ugorji/go/codec from 1.2.12 to 1.3.0 (#4268)
Bumps [github.com/ugorji/go/codec](https://github.com/ugorji/go) from 1.2.12 to 1.3.0.
- [Release notes](https://github.com/ugorji/go/releases)
- [Commits](https://github.com/ugorji/go/compare/v1.2.12...codec/v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/ugorji/go/codec
  dependency-version: 1.3.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-07-13 09:41:31 +08:00
Denny Septian Panggabean
dbd8a25150
feat: added AbortWithStatusPureJSON() in Context (#4290)
* feat: added `AbortWithStatusPureJSON()` in context

* Update context_test.go
2025-07-13 09:40:35 +08:00
dependabot[bot]
b7d6308bcc
chore(deps): bump github.com/quic-go/quic-go from 0.52.0 to 0.53.0 (#4281)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.52.0 to 0.53.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.53.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-07-13 09:27:07 +08:00
Denny Septian Panggabean
4bdcd9d0f1
docs: added available ID documentation (#4287) 2025-07-13 09:26:26 +08:00
Denny Septian Panggabean
76dd08d512
docs: wrong badge workflow in README.md (#4286) 2025-07-07 17:20:47 +08:00
M. Ilham Surya Pratama
cf4775283e
chroe: migrate yaml package to github.com/goccy/go-yaml (#4272) 2025-06-21 12:38:28 +08:00
Tim
688a429d19
feat: support custom json codec at runtime (#3391)
* refactor(json): export json codec

* feat(json): support custom json codec at runtime

* chore(copyright): update copyright to 2025 gin core team

* docs(gin): add custom json codec examples in doc file
2025-06-16 23:16:36 +08:00
dependabot[bot]
0a864884de
chore(deps): bump golang.org/x/net from 0.40.0 to 0.41.0 (#4262)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.41.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-06-16 23:01:46 +08:00
Victor Dusart
dd33ff7938
fix(docs): missing go markdown codeblock (#4266) 2025-06-16 22:59:53 +08:00
Name
77d70e5858
refactor(internal/bytesconv): replace rand usage with crypto/rand and rand.Int63 (#4259)
Co-authored-by: huangzw <huangzw@2345.com>
2025-06-09 21:05:34 +08:00
eqsdxr
a9c5b36578
docs: small changes (#4261) 2025-06-09 21:04:23 +08:00
OHZEKI Naoki
e30123ad73
refactor(recovery): extract Authorization header masking into maskAuthorization func (#4143)
* refactor(recovery): extract Authorization header masking into maskAuthorization func

* test(recovery): Add a test for maskAuthorization
2025-06-02 12:38:19 +08:00
Name
3c12d2a80e
perf(recover): replace bytes with strings in function for better performance (#4252)
Co-authored-by: huangzw <huangzw@2345.com>
2025-05-31 08:41:13 +08:00
dependabot[bot]
61c2b1c28f
chore(deps): bump github.com/quic-go/quic-go from 0.51.0 to 0.52.0 (#4250)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.51.0 to 0.52.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.51.0...v0.52.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.52.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-05-27 20:27:26 +08:00
Flc゛
41d8591eb1
refactor(context): refactor Keys type to map[any]any (#3963)
* refactor(context): refactor keys to `map[any]any`

Signed-off-by: Flc゛ <four_leaf_clover@foxmail.com>

* refactor(context): refactor keys to `map[any]any`

Signed-off-by: Flc゛ <four_leaf_clover@foxmail.com>

* style(context): remove empty lines before GetInt16, GetIntSlice, and GetStringMapString methods

- Remove unnecessary empty lines in the context.go file
- Improve code readability and consistency

Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>

* refactor(context): simplify GetStringSlice function

- Replace manual type assertion with generic getTyped function
- Reduce code duplication and improve type safety

Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>

* test(context): improve context.Set and context.Get tests

- Split existing test into separate functions for different scenarios
- Add test for setting and getting values with any key type
- Add test for handling non-comparable keys
- Improve assertions to check for key existence and value correctness

Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>

* refactor(context): replace fmt.Errorf with fmt.Sprintf in panic message

* test(context): remove trailing hyphen from context_test.go

* refactor(context): improve error message for missing key in context

- Remove unnecessary quotes around the key in the error message
- Simplify the error message format for better readability

* test(context): improve panic test message for non-existent key

---------

Signed-off-by: Flc゛ <four_leaf_clover@foxmail.com>
Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>
2025-05-26 23:15:14 +08:00
Flc゛
848e1cdd0d
refactor: replace interface{} with any in type declarations (#4249)
- Update golangci.yml to use 'any' instead of 'interface{}' in gofmt
- Modify debug.go, plain.go, and render_test.go to use 'any' type
- Improve code readability and follow modern Go conventions

Signed-off-by: Flc <four_leaf_clover@foxmail.com>
2025-05-26 23:11:05 +08:00
Flc゛
c8af82af15
test(context): add cleanup for uploaded file in SaveUploadedFile test (#4248)
Signed-off-by: Flc <four_leaf_clover@foxmail.com>
2025-05-25 20:38:39 +08:00
Alessandro (Ale) Segala
40725d85ba
chore(bind): return 413 status code when error is http.MaxBytesError (#4227)
* Bind: return 413 status code when error is `http.MaxBytesError`

The Go standard library includes a method `http.MaxBytesReader` that allows limiting the request body. For example, users can create a middleware like:

```go
func MiddlewareMaxBodySize(c *gin.Context) {
	// Limit request body to 100 bytes
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 100)
	c.Next()
}
```

When the body exceeds the limit, reading from the request body returns an error of type `http.MaxBytesError`.

This PR makes sure that when the error is of kind `http.MaxBytesError`, Gin returns the correct status code 413 (Request Entity Too Large) instead of a generic 400 (Bad Request).

* Disable test when using sonic

* Fix

* Disable for go-json too

* Add references to GitHub issues

* Test that the response is 400 for sonic and go-json
2025-05-25 20:36:33 +08:00
Flc゛
c4287b1300
ci(golangci-lint): update configuration and fix lint issues (#4247)
* ci: update golangci-lint configuration and lint settings

- Update golangci-lint to version 2
- Enable new linters and adjust existing ones
- Update lint settings across multiple test files
- Remove unused struct and variable checks
- Add new lint exclusions for generated code and specific directories

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* ci(github): update golangci-lint-action to v8 and lint version to v2.3.4

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* ci: downgrade golangci-lint to v2.1.6

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* ci(golangci): add gofumpt linter and fix related issues- Added gofumpt linter to .golangci.yml

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* test: ignore testifylint and gofumpt lints in specific test cases

Signed-off-by: Flc <four_leaf_clover@foxmail.com>

* build(deps): remove golang.org/x/lint

- Remove golang.org/x/lint package from go.mod
- Update related dependencies in go.sum

Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>

* build(deps): downgrade golang.org/x/mod and golang.org/x/tools

- Downgrade golang.org/x/mod from v0.24.0 to v0.18.0
- Downgrade golang.org/x/tools from v0.33.0 to v.22.0

These changes are made to address compatibility issues with the current project setup.

Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>

---------

Signed-off-by: Flc <four_leaf_clover@foxmail.com>
Signed-off-by: flc1125 <four_leaf_clover@foxmail.com>
2025-05-23 14:46:48 +08:00
Bo-Yi Wu
8fb3136664
Revert "fix(time): binding time with empty value (#4103)" (#4245)
This reverts commit 674522db91d637d179c16c372d87756ea26fa089.
2025-05-22 19:20:04 +08:00
Kashiwa
674522db91
fix(time): binding time with empty value (#4103)
* fix: binding time with empty value (#4098)

* refact: simplify null-to-zero filling logic

* test: add test for zeroUnixNanoTime
2025-05-21 19:21:46 +08:00
Andreas Deininger
8f7c340577
context_test.go: fix useless asserts (#4115) 2025-05-21 19:16:29 +08:00
yangquanshi
d00e6a5695
chore: fix some function names in comment (#4131)
Signed-off-by: yangquanshi <yangquan@52it.net>
2025-05-21 19:14:28 +08:00
Liu Ziming
19f5a13fb4
docs(readme): add gin-gonic/contrib (#4134) 2025-05-21 08:25:00 +08:00
NARITA
fb09c825e8
feat(context): add SetCookieData (#4240)
* feat(context): add SetCookieStruct (#4215)# This is a combination of 2 commits.

feat(context): add SetCookieStruct (#4215)

feat(context): add SetCookieStruct (#4215)

* feat(context): add SetCookieStruct (#4215)

* feat(context): fix SetCookieStruct→SetCookieData (gin-gonic#4215)

* fix(context): respect caller-specified SameSite value in SetCookieData
2025-05-21 08:20:44 +08:00
Name
3d8e288c64
perf(all): use strings.Cut to replace strings.SplitN (#4239)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-05-20 22:58:34 +08:00
Salim Absi
2e2bd1f408
test(internal/fs): fix test function name (#4235) 2025-05-20 18:29:39 +08:00
Siddhesh Mhadnak
da67cc1b98
test: fix lint failures (#4244) 2025-05-20 18:16:21 +08:00
dependabot[bot]
ef68fa032c
chore(deps): bump golang.org/x/net from 0.38.0 to 0.40.0 (#4229)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.40.0.
- [Commits](https://github.com/golang/net/compare/v0.38.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.40.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-05-11 22:42:01 +08:00
Orkhan Alikhanov
b38c59de7f
fix(errors): change Unwrap method receiver to value type (#4232) 2025-05-11 22:38:33 +08:00
dependabot[bot]
cf32d2dcf8
chore(deps): bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.4 (#4212)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.2 to 2.2.4.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.2...v2.2.4)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.2.4
  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-05-11 22:35:03 +08:00
dependabot[bot]
4714c2a9a3
chore(deps): bump google.golang.org/protobuf from 1.34.1 to 1.36.6 (#4198)
Bumps google.golang.org/protobuf from 1.34.1 to 1.36.6.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  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-05-11 22:34:39 +08:00
Yash
7a1b655074
fix: sonic on arm64 (#4234) 2025-05-11 22:34:09 +08:00
Name
67c9d4ee11
refactor: replace magic number 128 with unicode.MaxASCII in AsciiJSON Render (#4224)
Co-authored-by: huangzw <huangzw@2345.com>
2025-04-21 22:05:28 +08:00
dependabot[bot]
bb82473103
chore(deps): bump github.com/quic-go/quic-go from 0.48.2 to 0.50.1 (#4197)
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.48.2 to 0.50.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.48.2...v0.50.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  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-04-21 00:15:51 +08:00
dependabot[bot]
255af882db
chore(deps): bump github.com/go-playground/validator/v10 (#4208)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.22.1 to 10.26.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.22.1...v10.26.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-version: 10.26.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-04-21 00:14:45 +08:00
sunshineplan
71496abe68
feat(fs): Implement loading HTML from http.FileSystem (#4053)
* Implement loading HTML from http.FileSystem

* Add OnlyHTMLFS test

* Move OnlyHTMLFS to internal and add test
2025-04-21 00:11:16 +08:00
Name
0eb99493c2
perf: optimize AsciiJSON.Render method by using fmt.Appendf and reusing temp buffer (#4175)
per: use bytesconv.BytesToString(ret) instead of string(str)

Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-04-21 00:05:34 +08:00
dependabot[bot]
afa0c31d97
chore(deps): bump github.com/gin-contrib/sse from 0.1.0 to 1.1.0 (#4216)
Bumps [github.com/gin-contrib/sse](https://github.com/gin-contrib/sse) from 0.1.0 to 1.1.0.
- [Release notes](https://github.com/gin-contrib/sse/releases)
- [Changelog](https://github.com/gin-contrib/sse/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/sse/compare/v0.1.0...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/sse
  dependency-version: 1.1.0
  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>
2025-04-21 00:02:02 +08:00
dependabot[bot]
56fccc39ec
chore(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 (#4221)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 00:01:35 +08:00
eduardo-ax
3319038418
fix(readme): fix broken link to English documentation (#4222)
Co-authored-by: Eduardo Alexandre <eduardoalexandree.ps>
2025-04-21 00:01:03 +08:00
NezhaFan
49e9137c68
docs: fix comment (#4205)
Co-authored-by: voyager1 <voyager1@voyager1deMacBook-Pro.local>
2025-04-12 00:00:59 +08:00
Adlai Bridson-Boyczuk
1b53a47790
docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md (#4207)
* docs: Fixing grammar mistakes and awkward sentences, such as modeling
binding section

* Update doc.md

Missed grammar mistake
2025-04-11 23:59:03 +08:00
Andrey Bolonin
3afff295a2
docs: add Upd language list (#4211)
* Upd language list

* Update url
2025-04-11 23:58:02 +08:00
bound2
8763f33c65
fix: prevent middleware re-entry issue in HandleContext (#3987) 2025-03-20 23:40:41 +08:00
revevide
e737e3e267
fix(binding): prevent duplicate decoding and add validation in decodeToml (#4193) 2025-03-20 23:35:49 +08:00
takanuva15
4ccfa7c275
feat(binding): add support for unixMilli and unixMicro (#4190) 2025-03-20 23:33:10 +08:00
Bo-Yi Wu
90cf460269
chore: update Go versions and dependencies for improved compatibility (#4187)
* chore: update Go versions and dependencies for improved compatibility

- Update Go versions in workflow file to `1.23` and `1.24`
- Enhance test tags in workflow with specific linker flags
- Remove the conditional formatting step for Go `1.22.x` in workflow
- Remove `goimports` settings from `.golangci.yml`
- Update `go.mod` to use Go `1.23.0`
- Upgrade `github.com/bytedance/sonic` from `v1.11.6` to `v1.13.1`
- Update indirect dependencies `sonic/loader` to `v0.2.4` and `base64x` to `v0.1.5` in `go.mod`

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

* chore: update project for Go 1.23 compatibility and documentation fixes

- Update Go version requirement from 1.22 to 1.23 in README.md
- Remove superfluous `$` from example command in README.md
- Update warning message to reflect new Go version requirement in debug.go
- Update test assertion to reflect new Go version requirement in debug_test.go

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

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-03-20 10:13:47 +08:00
Name
ebe5e2a6bf
fix(golangci.yml): move fiximports to goimports section and replace exportloopref with copyloopvar (#4167)
Co-authored-by: huangzw <huangzw@2345.com>
2025-03-18 23:13:03 +08:00
dependabot[bot]
733ee094fc
chore(deps): bump golang.org/x/net from 0.33.0 to 0.37.0 (#4178)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  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-03-18 22:15:13 +08:00
NezhaFan
a4baac6e5e
refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile (#4181)
Co-authored-by: voyager1 <voyager1@voyager1deMacBook-Pro.local>
2025-03-18 22:14:38 +08:00
NezhaFan
1eb827240e
docs: fix case error of X-Real-IP (#4185)
Co-authored-by: voyager1 <voyager1@voyager1deMacBook-Pro.local>
2025-03-18 22:12:36 +08:00
Bo-Yi Wu
3b28645dc9
ci: add go version 1.24 to GitHub Actions (#4154)
- Add Go version `1.24` to the GitHub Actions workflow

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-02-12 10:22:02 +08:00
dependabot[bot]
c3c8620a7f
chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 (#4052)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.20.0 to 10.22.1.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.20.0...v10.22.1)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  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-02-12 08:50:51 +08:00
Bo-Yi Wu
3f818c3fa6
chore(security): upgrade quic-go version to 0.48.2 (#4127)
- Update Go versions in GitHub Actions workflow to `1.22` and `1.23`
- Update README to require Go version `1.22` or above
- Adjust table formatting in README for better alignment
- Update warning message in `debug.go` to reflect Go version `1.22`
- Update test in `debug_test.go` to reflect Go version `1.22`
- Update `go.mod` to require Go version `1.22`
- Update dependencies in `go.mod` to newer versions

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-12-30 11:40:37 +08:00
Bo-Yi Wu
23d6961aeb
ci(lint): update workflows and improve test request consistency (#4126)
- Update GoReleaser action to version 6 in GitHub workflow
- Use `http.MethodPost` constant in test requests instead of hardcoded string

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-12-30 11:39:24 +08:00
Xianglin Gao
e2e80f3347
chore(security): update vendor to fix CVE (#4121)
Signed-off-by: Xianglin Gao <xianglingao@tencent.com>
2024-12-28 17:18:03 +08:00
haesuo566
e46bd52185
refactor(context): add an optional permission parameter to the SaveUploadedFile method (#4068) (#4088)
Co-authored-by: hso <hso@trinitysoft.co.kr>
2024-11-15 23:54:06 +08:00
Matthieu MOREL
e8d34d053f
ci(lint): enable usestdlibvars linter (#4091)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2024-11-15 23:52:16 +08:00
Matthieu MOREL
02c1144f31
ci(lint): enable perfsprint linter (#4090)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2024-11-15 23:51:12 +08:00
Bo-Yi Wu
f875d87283
chore(context): test context initialization and handler logic (#4087)
* enhance code imported by #3413

if it needs to check if the handler is nil, tie c.index shall
always ++

* test: refactor test context initialization and handler logic

- Remove an empty line in `TestContextInitQueryCache`
- Add `TestContextNext` function with tests for `Next` method behavior with no handlers, one handler, and multiple handlers

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: zjj <zhong2plus@gmail.com>
2024-11-15 23:49:08 +08:00
Konovalov Maxim
c8a3adc657
refactor(context): simplify "GetType()" functions (#4080)
This PR introduces a generic function, getTyped[T any], to simplify value retrieval in the Context struct. It replaces repetitive type assertions in the GetString  GetBool etc. methods.

Co-authored-by: Maksim Konovalov <maksim.konovalov@vk.team>
2024-10-29 23:24:53 +08:00
Xinyu Kuo
ea53388e6e
fix(tree): Keep panic infos consistent when wildcard type build faild (#4077) 2024-10-26 08:28:59 +08:00
Oskar Karpiński
9d11234efe
docs(gin): Replace broken link to documentation with valid (#4064) 2024-10-26 08:26:25 +08:00
Xinyu Kuo
647311aba2
refactor(context): refactor context handling and improve test robustness (#4066)
Use assert.InDelta for float comparison with tolerance in TestContextGetFloat32
Remove unnecessary blank line in TestContextInitQueryCache
Replace anonymous struct with named contextKey type in TestContextWithFallbackValueFromRequestContext
Update context key handling in TestContextWithFallbackValueFromRequestContext to use contextKey type
2024-10-25 09:33:31 +08:00
tsukasa-ino
299c6f30e3
docs: trimmed some white spaces (#4070) 2024-10-25 09:16:40 +08:00
Enzo Lanzellotti
b080116a7f
docs(readme): add Portuguese documentation. (#4078) 2024-10-25 09:08:11 +08:00
wangjingcun
ad740d508f
docs(context): fix some function names in comment (#4079) 2024-10-25 09:07:03 +08:00
takanuva15
f05f966a08
feat(form): Support default values for collections in form binding (#4048) 2024-09-21 23:24:18 +08:00
CC11001100
9d7c0e9e1a
feat(context): GetXxx added support for more go native types (#3633) 2024-09-15 08:58:59 +08:00
demouth
f2c861a24f
docs: fix route group example code (#4020) 2024-09-15 08:54:23 +08:00
Ahmad Saeed Goda
28e57f58b1
fix(form): Set default value for form fields (#4047)
- Use specified default value in struct tags when binding a request input to struct for validation, even if sent empty, not only when not sent at all.
- Add string field to `TestMappingDefault` test case.
- Add test case for not sent form field to default to the value specified via code.
- Add test case for form field sent empty to default to the value specified via code.

Fixes: How to apply default value if empty value provided by client during model binding? #4042, #13042df, #a41721a
2024-09-06 13:21:19 +08:00
Jo YoHan
3cb30679b5
feat(form): add array collection format in form binding (#3986)
* feat(form): add array collection format in form binding

* feat(form): add array collection format in form binding

* test(form): fix test code for array collection format in form binding
2024-08-24 14:16:30 +08:00
dependabot[bot]
cc4e11438c
chore(deps): bump golang.org/x/net from 0.25.0 to 0.27.0 (#4013)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.25.0 to 0.27.0.
- [Commits](https://github.com/golang/net/compare/v0.25.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  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>
2024-07-14 20:34:34 +08:00
Matthieu MOREL
5f55c6a711
ci(lint): enable testifylint linter (#4010)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2024-07-14 20:33:08 +08:00
Pierre-Henri Symoneaux
626d55b0c0
fix(gin): Do not panic when handling method not allowed on empty tree (#4003)
Signed-off-by: Pierre-Henri Symoneaux <pierre-henri.symoneaux@ovhcloud.com>
2024-06-22 22:19:04 +08:00
demouth
9c081de9cd
docs: fix typo in Gin Quick Start (#3997) 2024-06-16 00:28:08 +08:00
Meng Zhuo
64ead9e6bd
docs(readme): replace godoc with pkg (#3985)
* Update README.md
2024-06-06 17:10:03 +08:00
wssccc
4621b7ac98
feat(router): add literal colon support (#1432) (#2857) 2024-06-01 13:44:57 +08:00
Endless Paradox
334160bab7
chore(tree): replace the self-defined 'min' to official one (#3975) 2024-05-24 14:55:25 +08:00
bruceNu1l
24d67647cb
feat(form): add custom string slice for form tag unmarshal (#3970) (#3971)
Co-authored-by: Bruce Lee <admin@ifocusad.com>
2024-05-23 10:16:11 +08:00
Adriano Sela Aviles
e0d46ded6c
fix(context): verify URL is Non-nil in initQueryCache() (#3969) 2024-05-19 10:48:07 +08:00
RedCrazyGhost
4f339e6a35
fix(context): YAML judgment logic in Negotiate (#3966) 2024-05-14 10:25:54 +08:00
51pwn
36b0dede4b
fix(context): check handler is nil (#3413)
* fixed #3404 2022-11-23

* up 2022-11-23

* refactor: refactor context handling and nil checks

- Refactor nil checks to improve readability in `context.go`
- Modify the control flow in `HandlerNames` and `Next` methods to continue on nil values before appending or invoking handlers in `context.go`

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

* test: refactor context_test.go for clarity and efficiency

- Insert a `nil` value into the `HandlersChain` array in `context_test.go`
- Remove empty test functions in `context_test.go`

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-13 14:55:41 +08:00
Name
3f5b0afa2a
refactor(slice): simplify SliceValidationError Error method (#3910)
* Simplify SliceValidationError Error method

* Replace fmt.Fprintf with b.WriteString

---------

Co-authored-by: huangzw <huangzw@hsmap.com>
Co-authored-by: 1911860538 <alxps1911@163.com>
2024-05-13 13:32:46 +08:00
crunchyfrog
a569ed8f26
docs(readme): fix language and moved link (#3962)
* Update README.md

* more fixes & fix moved link
2024-05-13 11:12:55 +08:00
guonaihong
6ca8ddb1ae
feat(binding): add BindPlain (#3904)
* add BindPlain

* fix ci/cd error
2024-05-13 11:11:56 +08:00
Mobin Mohanan
40131af124
ci(Makefile): added help and descriptions to targets (#3964) 2024-05-13 09:29:21 +08:00
thinkerou
c677ccc40a
fix(go): invalid Go toolchain version (#3961) 2024-05-10 07:27:42 +08:00
Bo-Yi Wu
7e298066ba
build: update Gin minimum Go version to 1.21 (#3960)
* build: update Gin minimum Go version to 1.21

- Update the minimum Go version requirement for Gin from `1.20` to `1.21` in both `debug.go` and `debug_test.go`
- Modify the warning message to reflect the new minimum Go version requirement in `debug.go`
- Adjust the test assertion to match the updated warning message in `debug_test.go`

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

* docs: refine project documentation and CI configurations

- Update supported Go versions for GitHub actions to `1.21` and `1.22`
- Specify the required Go version as `1.21` or above in README
- Change code block syntax to `sh` in installation and demo run instructions
- Remove empty lines in README sections
- Update project list formatting without changing the content

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-09 13:45:03 +08:00
thinkerou
3ac729dc4a
feat(gin): support http3 using quic-go/quic-go (#3210)
* experimental support http3

* remove go1.14 and go1.15

* update quic-go package path

* only support go1.19+

* remove go19 support

* update gomod

* chore: refine CI configuration and dependencies

- Remove dynamic Go versioning in favor of pinning to major version `1`
- Update linter version from `v1.56.2` to `v1.58.1` in GitHub Actions workflow

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

* chore: refactor CI workflow and improve tests

- Update the golangci-lint-action version from `v5` to `v6` in the GitHub workflow configuration

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

* chore: update dependencies and CI configurations

- Update Go version requirement from `1.20` to `1.21` in `go.mod`

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

* style: refactor codebase and update tests

- Add an empty line in the import section of `gin.go`

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

* chore: enhance code quality and consistency

- Add `gin.go` to the list of files with specific linters in `.golangci.yml`, applying the `gci` linter.

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-09 09:17:06 +08:00
Johannes Eiglsperger
8791c96960
feat(fs): Export, test and document OnlyFilesFS (#3939) 2024-05-08 15:47:54 +08:00
Bo-Yi Wu
b1c1e7b572
ci: update Go version requirements and remove test files (#3957)
- Update the Go version requirements in `.github/workflows/gin.yml`
- Remove test files for Go versions 1.18 and 1.19
- Update the required Go version in `debug.go` and `debug_test.go`
- Rename and modify files related to Go version 1.19 and 1.20 in the `internal/bytesconv` directory

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-08 10:14:42 +08:00
Kostadin Plachkov
7d147928ee
fix(gin): data race warning for gin mode (#1580)
* fix: data race warning (#1180)

* Fix the tests

* refactor: remove unnecessary imports and optimize codebase

- Remove unnecessary import of `flag`

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

* test: refactor test assertions for mode settings

- Update test assertions for mode setting in `mode_test.go`

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-08 09:13:36 +08:00
Pedro Aguiar
f5f5da8fa0
docs(gin): update link to dont-trust-all-proxies section (#3938) (#3945)
Update link [1] to [2] after PR #3449 was merged.

[1] https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies
[2] https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies

Closes
2024-05-08 06:31:01 +08:00
lgbgbl
8dd088927a
refactor(binding): use strings.Cut to replace strings.Index (#3522) 2024-05-08 06:28:15 +08:00
Flc゛
e60113dc95
docs(engine): fix comments for the With (#3955)
Signed-off-by: Flc゛ <four_leaf_clover@foxmail.com>
2024-05-08 05:29:54 +08:00
Bo-Yi Wu
490accf5d7
docs: update documentation and release notes for Gin v1.10.0 (#3953)
* docs: update documentation and release notes for Gin v1.10.0

- Add release notes for Gin v1.10.0
- Include new features and bug fixes in the changelog
- Document enhancements and build process updates
- Update documentation for context and middleware functions
- Upgrade dependencies and optimize unit tests

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

* feat: refactor CI, enhance file binding, and update dependencies

- Add proxy-server authentication feature
- Add support for custom BindUnmarshaler for binding
- Fix binding error while not uploading file
- Refactor CI and update dependencies
- Add support for RFC 9512: application/yaml
- Optimize the Copy method of the Context struct
- Update various Go dependencies to latest versions

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

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2024-05-07 12:50:01 +08:00
102 changed files with 5911 additions and 1781 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: # Pull Request Checklist
- Open your pull request against `master`
- Your pull request should have no more than two commits, if not you should squash them.
- It should pass all tests in the available continuous integration systems such as GitHub Actions.
- You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README.
Please ensure your pull request meets the following requirements:
- [ ] Open your pull request against the `master` branch.
- [ ] All tests pass in available continuous integration systems (e.g., GitHub Actions).
- [ ] Tests are added or modified as needed to cover code changes.
- [ ] If the pull request introduces a new feature, the feature is documented in the `docs/doc.md`.
Thank you for contributing!

View File

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

View File

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

View File

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

View File

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

56
.github/workflows/trivy-scan.yml vendored Normal file
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.33.1
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1
if: always()
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'table'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
exit-code: '1'

View File

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

View File

@ -1,5 +1,173 @@
# Gin ChangeLog # Gin ChangeLog
## Gin v1.11.0
### Features
* feat(gin): Experimental support for HTTP/3 using quic-go/quic-go ([#3210](https://github.com/gin-gonic/gin/pull/3210))
* feat(form): add array collection format in form binding ([#3986](https://github.com/gin-gonic/gin/pull/3986)), add custom string slice for form tag unmarshal ([#3970](https://github.com/gin-gonic/gin/pull/3970))
* feat(binding): add BindPlain ([#3904](https://github.com/gin-gonic/gin/pull/3904))
* feat(fs): Export, test and document OnlyFilesFS ([#3939](https://github.com/gin-gonic/gin/pull/3939))
* feat(binding): add support for unixMilli and unixMicro ([#4190](https://github.com/gin-gonic/gin/pull/4190))
* feat(form): Support default values for collections in form binding ([#4048](https://github.com/gin-gonic/gin/pull/4048))
* feat(context): GetXxx added support for more go native types ([#3633](https://github.com/gin-gonic/gin/pull/3633))
### Enhancements
* perf(context): optimize getMapFromFormData performance ([#4339](https://github.com/gin-gonic/gin/pull/4339))
* refactor(tree): replace string(/) with "/" in node.insertChild ([#4354](https://github.com/gin-gonic/gin/pull/4354))
* refactor(render): remove headers parameter from writeHeader ([#4353](https://github.com/gin-gonic/gin/pull/4353))
* refactor(context): simplify "GetType()" functions ([#4080](https://github.com/gin-gonic/gin/pull/4080))
* refactor(slice): simplify SliceValidationError Error method ([#3910](https://github.com/gin-gonic/gin/pull/3910))
* refactor(context):Avoid using filepath.Dir twice in SaveUploadedFile ([#4181](https://github.com/gin-gonic/gin/pull/4181))
* refactor(context): refactor context handling and improve test robustness ([#4066](https://github.com/gin-gonic/gin/pull/4066))
* refactor(binding): use strings.Cut to replace strings.Index ([#3522](https://github.com/gin-gonic/gin/pull/3522))
* refactor(context): add an optional permission parameter to SaveUploadedFile ([#4068](https://github.com/gin-gonic/gin/pull/4068))
* refactor(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
* refactor(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
* tree: replace the self-defined 'min' to official one ([#3975](https://github.com/gin-gonic/gin/pull/3975))
* context: Remove redundant filepath.Dir usage ([#4181](https://github.com/gin-gonic/gin/pull/4181))
### Bug Fixes
* fix: prevent middleware re-entry issue in HandleContext ([#3987](https://github.com/gin-gonic/gin/pull/3987))
* fix(binding): prevent duplicate decoding and add validation in decodeToml ([#4193](https://github.com/gin-gonic/gin/pull/4193))
* fix(gin): Do not panic when handling method not allowed on empty tree ([#4003](https://github.com/gin-gonic/gin/pull/4003))
* fix(gin): data race warning for gin mode ([#1580](https://github.com/gin-gonic/gin/pull/1580))
* fix(context): verify URL is Non-nil in initQueryCache() ([#3969](https://github.com/gin-gonic/gin/pull/3969))
* fix(context): YAML judgment logic in Negotiate ([#3966](https://github.com/gin-gonic/gin/pull/3966))
* fix(context): check handler is nil ([#3413](https://github.com/gin-gonic/gin/pull/3413))
* fix(readme): fix broken link to English documentation ([#4222](https://github.com/gin-gonic/gin/pull/4222))
* fix(tree): Keep panic infos consistent when wildcard type build faild ([#4077](https://github.com/gin-gonic/gin/pull/4077))
### Build process updates / CI
* ci: integrate Trivy vulnerability scanning into CI workflow ([#4359](https://github.com/gin-gonic/gin/pull/4359))
* ci: support Go 1.25 in CI/CD ([#4341](https://github.com/gin-gonic/gin/pull/4341))
* build(deps): upgrade github.com/bytedance/sonic from v1.13.2 to v1.14.0 ([#4342](https://github.com/gin-gonic/gin/pull/4342))
* ci: add Go version 1.24 to GitHub Actions ([#4154](https://github.com/gin-gonic/gin/pull/4154))
* build: update Gin minimum Go version to 1.21 ([#3960](https://github.com/gin-gonic/gin/pull/3960))
* ci(lint): enable new linters (testifylint, usestdlibvars, perfsprint, etc.) ([#4010](https://github.com/gin-gonic/gin/pull/4010), [#4091](https://github.com/gin-gonic/gin/pull/4091), [#4090](https://github.com/gin-gonic/gin/pull/4090))
* ci(lint): update workflows and improve test request consistency ([#4126](https://github.com/gin-gonic/gin/pull/4126))
### Dependency updates
* chore(deps): bump google.golang.org/protobuf from 1.36.6 to 1.36.9 ([#4346](https://github.com/gin-gonic/gin/pull/4346), [#4356](https://github.com/gin-gonic/gin/pull/4356))
* chore(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 ([#4347](https://github.com/gin-gonic/gin/pull/4347))
* chore(deps): bump actions/setup-go from 5 to 6 ([#4351](https://github.com/gin-gonic/gin/pull/4351))
* chore(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.0 ([#4328](https://github.com/gin-gonic/gin/pull/4328))
* chore(deps): bump golang.org/x/net from 0.33.0 to 0.38.0 ([#4178](https://github.com/gin-gonic/gin/pull/4178), [#4221](https://github.com/gin-gonic/gin/pull/4221))
* chore(deps): bump github.com/go-playground/validator/v10 from 10.20.0 to 10.22.1 ([#4052](https://github.com/gin-gonic/gin/pull/4052))
### Documentation updates
* docs(changelog): update release notes for Gin v1.10.1 ([#4360](https://github.com/gin-gonic/gin/pull/4360))
* docs: Fixing English grammar mistakes and awkward sentence structure in doc/doc.md ([#4207](https://github.com/gin-gonic/gin/pull/4207))
* docs: update documentation and release notes for Gin v1.10.0 ([#3953](https://github.com/gin-gonic/gin/pull/3953))
* docs: fix typo in Gin Quick Start ([#3997](https://github.com/gin-gonic/gin/pull/3997))
* docs: fix comment and link issues ([#4205](https://github.com/gin-gonic/gin/pull/4205), [#3938](https://github.com/gin-gonic/gin/pull/3938))
* docs: fix route group example code ([#4020](https://github.com/gin-gonic/gin/pull/4020))
* docs(readme): add Portuguese documentation ([#4078](https://github.com/gin-gonic/gin/pull/4078))
* docs(context): fix some function names in comment ([#4079](https://github.com/gin-gonic/gin/pull/4079))
---
## Gin v1.10.1
### Features
* refactor: strengthen HTTPS security and improve code organization
* feat(binding): Support custom BindUnmarshaler for binding. (#3933)
### Enhancements
* chore(deps): bump github.com/bytedance/sonic from 1.11.3 to 1.11.6 (#3940)
* chore(deps): bump golangci/golangci-lint-action from 4 to 5 (#3941)
* chore: update external dependencies to latest versions (#3950)
* chore: update various Go dependencies to latest versions (#3901)
* chore: refactor configuration files for better readability (#3951)
* chore: update changelog categories and improve documentation (#3917)
* feat: update version constant to v1.10.0 (#3952)
### Build process updates
* ci(release): refactor changelog regex patterns and exclusions (#3914)
* ci(Makefile): vet command add .PHONY (#3915)
## Gin v1.10.0
### Features
* feat(auth): add proxy-server authentication (#3877) (@EndlessParadox1)
* feat(bind): ShouldBindBodyWith shortcut and change doc (#3871) (@RedCrazyGhost)
* feat(binding): Support custom BindUnmarshaler for binding. (#3933) (@dkkb)
* feat(binding): support override default binding implement (#3514) (@ssfyn)
* feat(engine): Added `OptionFunc` and `With` (#3572) (@flc1125)
* feat(logger): ability to skip logs based on user-defined logic (#3593) (@palvaneh)
### Bug fixes
* Revert "fix(uri): query binding bug (#3236)" (#3899) (@appleboy)
* fix(binding): binding error while not upload file (#3819) (#3820) (@clearcodecn)
* fix(binding): dereference pointer to struct (#3199) (@echovl)
* fix(context): make context Value method adhere to Go standards (#3897) (@FarmerChillax)
* fix(engine): fix unit test (#3878) (@flc1125)
* fix(header): Allow header according to RFC 7231 (HTTP 405) (#3759) (@Crocmagnon)
* fix(route): Add fullPath in context copy (#3784) (@KarthikReddyPuli)
* fix(router): catch-all conflicting wildcard (#3812) (@FirePing32)
* fix(sec): upgrade golang.org/x/crypto to 0.17.0 (#3832) (@chncaption)
* fix(tree): correctly expand the capacity of params (#3502) (@georgijd-form3)
* fix(uri): query binding bug (#3236) (@illiafox)
* fix: Add pointer support for url query params (#3659) (#3666) (@omkar-foss)
* fix: protect Context.Keys map when call Copy method (#3873) (@kingcanfish)
### Enhancements
* chore(CI): update release args (#3595) (@qloog)
* chore(IP): add TrustedPlatform constant for Fly.io. (#3839) (@ab)
* chore(debug): add ability to override the debugPrint statement (#2337) (@josegonzalez)
* chore(deps): update dependencies to latest versions (#3835) (@appleboy)
* chore(header): Add support for RFC 9512: application/yaml (#3851) (@vincentbernat)
* chore(http): use white color for HTTP 1XX (#3741) (@viralparmarme)
* chore(optimize): the ShouldBindUri method of the Context struct (#3911) (@1911860538)
* chore(perf): Optimize the Copy method of the Context struct (#3859) (@1911860538)
* chore(refactor): modify interface check way (#3855) (@demoManito)
* chore(request): check reader if it's nil before reading (#3419) (@noahyao1024)
* chore(security): upgrade Protobuf for CVE-2024-24786 (#3893) (@Fotkurz)
* chore: refactor CI and update dependencies (#3848) (@appleboy)
* chore: refactor configuration files for better readability (#3951) (@appleboy)
* chore: update GitHub Actions configuration (#3792) (@appleboy)
* chore: update changelog categories and improve documentation (#3917) (@appleboy)
* chore: update dependencies to latest versions (#3694) (@appleboy)
* chore: update external dependencies to latest versions (#3950) (@appleboy)
* chore: update various Go dependencies to latest versions (#3901) (@appleboy)
### Build process updates
* build(codecov): Added a codecov configuration (#3891) (@flc1125)
* ci(Makefile): vet command add .PHONY (#3915) (@imalasong)
* ci(lint): update tooling and workflows for consistency (#3834) (@appleboy)
* ci(release): refactor changelog regex patterns and exclusions (#3914) (@appleboy)
* ci(testing): add go1.22 version (#3842) (@appleboy)
### Documentation updates
* docs(context): Added deprecation comments to BindWith (#3880) (@flc1125)
* docs(middleware): comments to function `BasicAuthForProxy` (#3881) (@EndlessParadox1)
* docs: Add document to constant `AuthProxyUserKey` and `BasicAuthForProxy`. (#3887) (@EndlessParadox1)
* docs: fix typo in comment (#3868) (@testwill)
* docs: fix typo in function documentation (#3872) (@TotomiEcio)
* docs: remove redundant comments (#3765) (@WeiTheShinobi)
* feat: update version constant to v1.10.0 (#3952) (@appleboy)
### Others
* Upgrade golang.org/x/net -> v0.13.0 (#3684) (@cpcf)
* test(git): gitignore add develop tools (#3370) (@demoManito)
* test(http): use constant instead of numeric literal (#3863) (@testwill)
* test(path): Optimize unit test execution results (#3883) (@flc1125)
* test(render): increased unit tests coverage (#3691) (@araujo88)
## Gin v1.9.1 ## Gin v1.9.1
### BUG FIXES ### BUG FIXES
@ -414,7 +582,7 @@
- [FIX] Refactor render - [FIX] Refactor render
- [FIX] Reworked tests - [FIX] Reworked tests
- [FIX] logger now supports cygwin - [FIX] logger now supports cygwin
- [FIX] Use X-Forwarded-For before X-Real-Ip - [FIX] Use X-Forwarded-For before X-Real-IP
- [FIX] time.Time binding (#904) - [FIX] time.Time binding (#904)
## Gin 1.1.4 ## Gin 1.1.4

View File

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

View File

@ -8,6 +8,7 @@ TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | gr
TESTTAGS ?= "" TESTTAGS ?= ""
.PHONY: test .PHONY: test
# Run tests to verify code functionality.
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
@ -30,10 +31,12 @@ test:
done done
.PHONY: fmt .PHONY: fmt
# Ensure consistent code formatting.
fmt: fmt:
$(GOFMT) -w $(GOFILES) $(GOFMT) -w $(GOFILES)
.PHONY: fmt-check .PHONY: fmt-check
# format (check only).
fmt-check: fmt-check:
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
@ -43,31 +46,36 @@ fmt-check:
fi; fi;
.PHONY: vet .PHONY: vet
# Examine packages and report suspicious constructs if any.
vet: vet:
$(GO) vet $(VETPACKAGES) $(GO) vet $(VETPACKAGES)
.PHONY: lint .PHONY: lint
# Inspect source code for stylistic errors or potential bugs.
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u golang.org/x/lint/golint; \ $(GO) get -u golang.org/x/lint/golint; \
fi fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell-check
misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -error $(GOFILES)
.PHONY: misspell .PHONY: misspell
# Correct commonly misspelled English words in source code.
misspell: misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -w $(GOFILES) misspell -w $(GOFILES)
.PHONY: misspell-check
# misspell (check only).
misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -error $(GOFILES)
.PHONY: tools .PHONY: tools
# Install tools (golint and misspell).
tools: tools:
@if [ $(GO_VERSION) -gt 15 ]; then \ @if [ $(GO_VERSION) -gt 15 ]; then \
$(GO) install golang.org/x/lint/golint@latest; \ $(GO) install golang.org/x/lint/golint@latest; \
@ -76,3 +84,23 @@ tools:
$(GO) install golang.org/x/lint/golint; \ $(GO) install golang.org/x/lint/golint; \
$(GO) install github.com/client9/misspell/cmd/misspell; \ $(GO) install github.com/client9/misspell/cmd/misspell; \
fi fi
.PHONY: help
# Help.
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

189
README.md
View File

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

View File

@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil) req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password")) req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil) req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil) req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -147,7 +147,7 @@ func TestBasicAuthForProxySucceed(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil) req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password")) req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
@ -166,7 +166,7 @@ func TestBasicAuthForProxy407(t *testing.T) {
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil) req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)

View File

@ -14,21 +14,21 @@ import (
func BenchmarkOneRoute(B *testing.B) { func BenchmarkOneRoute(B *testing.B) {
router := New() router := New()
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping") runRequest(B, router, http.MethodGet, "/ping")
} }
func BenchmarkRecoveryMiddleware(B *testing.B) { func BenchmarkRecoveryMiddleware(B *testing.B) {
router := New() router := New()
router.Use(Recovery()) router.Use(Recovery())
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/") runRequest(B, router, http.MethodGet, "/")
} }
func BenchmarkLoggerMiddleware(B *testing.B) { func BenchmarkLoggerMiddleware(B *testing.B) {
router := New() router := New()
router.Use(LoggerWithWriter(newMockWriter())) router.Use(LoggerWithWriter(newMockWriter()))
router.GET("/", func(c *Context) {}) router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/") runRequest(B, router, http.MethodGet, "/")
} }
func BenchmarkManyHandlers(B *testing.B) { func BenchmarkManyHandlers(B *testing.B) {
@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/ping", func(c *Context) {}) router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping") runRequest(B, router, http.MethodGet, "/ping")
} }
func Benchmark5Params(B *testing.B) { func Benchmark5Params(B *testing.B) {
@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
router := New() router := New()
router.Use(func(c *Context) {}) router.Use(func(c *Context) {})
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {}) router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
runRequest(B, router, "GET", "/param/path/to/parameter/john/12345") runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345")
} }
func BenchmarkOneRouteJSON(B *testing.B) { func BenchmarkOneRouteJSON(B *testing.B) {
@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
router.GET("/json", func(c *Context) { router.GET("/json", func(c *Context) {
c.JSON(http.StatusOK, data) c.JSON(http.StatusOK, data)
}) })
runRequest(B, router, "GET", "/json") runRequest(B, router, http.MethodGet, "/json")
} }
func BenchmarkOneRouteHTML(B *testing.B) { func BenchmarkOneRouteHTML(B *testing.B) {
@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.GET("/html", func(c *Context) { router.GET("/html", func(c *Context) {
c.HTML(http.StatusOK, "index", "hola") c.HTML(http.StatusOK, "index", "hola")
}) })
runRequest(B, router, "GET", "/html") runRequest(B, router, http.MethodGet, "/html")
} }
func BenchmarkOneRouteSet(B *testing.B) { func BenchmarkOneRouteSet(B *testing.B) {
@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
router.GET("/ping", func(c *Context) { router.GET("/ping", func(c *Context) {
c.Set("key", "value") c.Set("key", "value")
}) })
runRequest(B, router, "GET", "/ping") runRequest(B, router, http.MethodGet, "/ping")
} }
func BenchmarkOneRouteString(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) {
@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
router.GET("/text", func(c *Context) { router.GET("/text", func(c *Context) {
c.String(http.StatusOK, "this is a plain text") c.String(http.StatusOK, "this is a plain text")
}) })
runRequest(B, router, "GET", "/text") runRequest(B, router, http.MethodGet, "/text")
} }
func BenchmarkManyRoutesFist(B *testing.B) { func BenchmarkManyRoutesFirst(B *testing.B) {
router := New() router := New()
router.Any("/ping", func(c *Context) {}) router.Any("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping") runRequest(B, router, http.MethodGet, "/ping")
} }
func BenchmarkManyRoutesLast(B *testing.B) { func BenchmarkManyRoutesLast(B *testing.B) {
@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
router := New() router := New()
router.Any("/something", func(c *Context) {}) router.Any("/something", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/ping") runRequest(B, router, http.MethodGet, "/ping")
} }
func Benchmark404Many(B *testing.B) { func Benchmark404Many(B *testing.B) {
@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
router.GET("/user/:id/:mode", func(c *Context) {}) router.GET("/user/:id/:mode", func(c *Context) {})
router.NoRoute(func(c *Context) {}) router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/viewfake") runRequest(B, router, http.MethodGet, "/viewfake")
} }
type mockWriter struct { type mockWriter struct {

View File

@ -84,6 +84,7 @@ var (
YAML BindingBody = yamlBinding{} YAML BindingBody = yamlBinding{}
Uri BindingUri = uriBinding{} Uri BindingUri = uriBinding{}
Header Binding = headerBinding{} Header Binding = headerBinding{}
Plain BindingBody = plainBinding{}
TOML BindingBody = tomlBinding{} TOML BindingBody = tomlBinding{}
) )

View File

@ -8,9 +8,11 @@ package binding
import ( import (
"bytes" "bytes"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -24,7 +26,7 @@ func TestBindingMsgPack(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf) assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test) err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err) require.NoError(t, err)
data := buf.Bytes() data := buf.Bytes()
@ -38,20 +40,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
assert.Equal(t, name, b.Name()) assert.Equal(t, name, b.Name())
obj := FooStruct{} obj := FooStruct{}
req := requestWithBody("POST", path, body) req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj) err := b.Bind(req, &obj)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{} obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody) req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK) req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj) err = MsgPack.Bind(req, &obj)
assert.Error(t, err) require.Error(t, err)
} }
func TestBindingDefaultMsgPack(t *testing.T) { func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK)) assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2)) assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2))
} }

View File

@ -81,6 +81,7 @@ var (
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{} Header = headerBinding{}
TOML = tomlBinding{} TOML = tomlBinding{}
Plain = plainBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@
package binding package binding
import ( import (
"fmt"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
@ -22,25 +22,20 @@ type SliceValidationError []error
// Error concatenates all error elements in SliceValidationError into a single string separated by \n. // Error concatenates all error elements in SliceValidationError into a single string separated by \n.
func (err SliceValidationError) Error() string { func (err SliceValidationError) Error() string {
n := len(err) if len(err) == 0 {
switch n {
case 0:
return "" return ""
default:
var b strings.Builder
if err[0] != nil {
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
}
if n > 1 {
for i := 1; i < n; i++ {
if err[i] != nil {
b.WriteString("\n")
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
}
}
}
return b.String()
} }
var b strings.Builder
for i := 0; i < len(err); i++ {
if err[i] != nil {
if b.Len() > 0 {
b.WriteString("\n")
}
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
}
}
return b.String()
} }
var _ StructValidator = (*defaultValidator)(nil) var _ StructValidator = (*defaultValidator)(nil)

View File

@ -12,11 +12,14 @@ import (
func BenchmarkSliceValidationError(b *testing.B) { func BenchmarkSliceValidationError(b *testing.B) {
const size int = 100 const size int = 100
for i := 0; i < b.N; i++ { e := make(SliceValidationError, size)
e := make(SliceValidationError, size) for j := 0; j < size; j++ {
for j := 0; j < size; j++ { e[j] = errors.New(strconv.Itoa(j))
e[j] = errors.New(strconv.Itoa(j)) }
}
b.ReportAllocs()
for b.Loop() {
if len(e.Error()) == 0 { if len(e.Error()) == 0 {
b.Errorf("error") b.Errorf("error")
} }

View File

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

View File

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

View File

@ -7,14 +7,15 @@ package binding
import ( import (
"errors" "errors"
"fmt" "fmt"
"maps"
"mime/multipart" "mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json"
) )
var ( var (
@ -159,6 +160,14 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
if k, v := head(opt, "="); k == "default" { if k, v := head(opt, "="); k == "default" {
setOpt.isDefaultExists = true setOpt.isDefaultExists = true
setOpt.defaultValue = v setOpt.defaultValue = v
// convert semicolon-separated default values to csv-separated values for processing in setByForm
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} }
} }
@ -167,7 +176,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method. // BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface { type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param. // UnmarshalParam decodes and assigns a value from a form or query param.
UnmarshalParam(param string) error UnmarshalParam(param string) error
} }
@ -182,6 +191,38 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil return false, nil
} }
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
return vs, nil
}
var sep string
switch cfTag {
case "csv":
sep = ","
case "ssv":
sep = " "
case "tsv":
sep = "\t"
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
}
totalLength := 0
for _, v := range vs {
totalLength += strings.Count(v, sep) + 1
}
newVs = make([]string, 0, totalLength)
for _, v := range vs {
newVs = append(newVs, strings.Split(v, sep)...)
}
return newVs, nil
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) { func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
vs, ok := form[tagValue] vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists { if !ok && !opt.isDefaultExists {
@ -190,17 +231,54 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() { switch value.Kind() {
case reflect.Slice: case reflect.Slice:
if !ok { if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
} }
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field)
case reflect.Array: case reflect.Array:
if !ok { if len(vs) == 0 {
if !opt.isDefaultExists {
return false, nil
}
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
vs = strings.Split(opt.defaultValue, ",")
}
} }
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
if vs, err = trySplit(vs, field); err != nil {
return false, err
}
if len(vs) != value.Len() { if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
} }
return true, setArray(vs, value, field) return true, setArray(vs, value, field)
default: default:
var val string var val string
@ -210,6 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if len(vs) > 0 { if len(vs) > 0 {
val = vs[0] val = vs[0]
if val == "" {
val = opt.defaultValue
}
} }
if ok, err := trySetCustom(val, value); ok { if ok, err := trySetCustom(val, value); ok {
return ok, err return ok, err
@ -219,6 +300,11 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
} }
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
// If it is a string type, no spaces are removed, and the user data is not modified here
if value.Kind() != reflect.String {
val = strings.TrimSpace(val)
}
switch value.Kind() { switch value.Kind() {
case reflect.Int: case reflect.Int:
return setIntField(val, 0, value) return setIntField(val, 0, value)
@ -259,9 +345,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case multipart.FileHeader: case multipart.FileHeader:
return nil return nil
} }
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr: case reflect.Ptr:
if !value.Elem().IsValid() { if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem())) value.Set(reflect.New(value.Type().Elem()))
@ -323,28 +409,34 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339 timeFormat = time.RFC3339
} }
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano": case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64) tv, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }
d := time.Duration(1) var t time.Time
if tf == "unixnano" { switch tf {
d = time.Second case "unix":
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
} }
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC l = time.UTC
@ -388,6 +480,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err
} }
func setTimeDuration(val string, value reflect.Value) error { func setTimeDuration(val string, value reflect.Value) error {
if val == "" {
val = "0"
}
d, err := time.ParseDuration(val) d, err := time.ParseDuration(val)
if err != nil { if err != nil {
return err return err
@ -397,11 +493,8 @@ func setTimeDuration(val string, value reflect.Value) error {
} }
func head(str, sep string) (head string, tail string) { func head(str, sep string) (head string, tail string) {
idx := strings.Index(str, sep) head, tail, _ = strings.Cut(str, sep)
if idx < 0 { return head, tail
return str, ""
}
return str[:idx], str[idx+len(sep):]
} }
func setFormMap(ptr any, form map[string][]string) error { func setFormMap(ptr any, form map[string][]string) error {
@ -412,9 +505,7 @@ func setFormMap(ptr any, form map[string][]string) error {
if !ok { if !ok {
return ErrConvertMapStringSlice return ErrConvertMapStringSlice
} }
for k, v := range form { maps.Copy(ptrMap, form)
ptrMap[k] = v
}
return nil return nil
} }

View File

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

View File

@ -5,7 +5,8 @@
package binding package binding
import ( import (
"fmt" "encoding/hex"
"errors"
"mime/multipart" "mime/multipart"
"reflect" "reflect"
"strconv" "strconv"
@ -14,6 +15,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMappingBaseTypes(t *testing.T) { func TestMappingBaseTypes(t *testing.T) {
@ -58,7 +60,7 @@ func TestMappingBaseTypes(t *testing.T) {
field := val.Elem().Type().Field(0) field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form") _, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
assert.NoError(t, err, testName) require.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface() actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName) assert.Equal(t, tt.expect, actual, testName)
@ -67,13 +69,15 @@ func TestMappingBaseTypes(t *testing.T) {
func TestMappingDefault(t *testing.T) { func TestMappingDefault(t *testing.T) {
var s struct { var s struct {
Str string `form:",default=defaultVal"`
Int int `form:",default=9"` Int int `form:",default=9"`
Slice []int `form:",default=9"` Slice []int `form:",default=9"`
Array [1]int `form:",default=9"` Array [1]int `form:",default=9"`
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "defaultVal", s.Str)
assert.Equal(t, 9, s.Int) assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array) assert.Equal(t, [1]int{9}, s.Array)
@ -84,7 +88,7 @@ func TestMappingSkipField(t *testing.T) {
A int A int
} }
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, s.A) assert.Equal(t, 0, s.A)
} }
@ -95,7 +99,7 @@ func TestMappingIgnoreField(t *testing.T) {
B int `form:"-"` B int `form:"-"`
} }
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form") err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B) assert.Equal(t, 0, s.B)
@ -107,7 +111,7 @@ func TestMappingUnexportedField(t *testing.T) {
b int `form:"b"` b int `form:"b"`
} }
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form") err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 9, s.A) assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b) assert.Equal(t, 0, s.b)
@ -118,7 +122,7 @@ func TestMappingPrivateField(t *testing.T) {
f int `form:"field"` f int `form:"field"`
} }
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form") err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, s.f) assert.Equal(t, 0, s.f)
} }
@ -128,7 +132,7 @@ func TestMappingUnknownFieldType(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form") err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
assert.Error(t, err) require.Error(t, err)
assert.Equal(t, errUnknownType, err) assert.Equal(t, errUnknownType, err)
} }
@ -137,7 +141,7 @@ func TestMappingURI(t *testing.T) {
F int `uri:"field"` F int `uri:"field"`
} }
err := mapURI(&s, map[string][]string{"field": {"6"}}) err := mapURI(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
@ -146,16 +150,34 @@ func TestMappingForm(t *testing.T) {
F int `form:"field"` F int `form:"field"`
} }
err := mapForm(&s, map[string][]string{"field": {"6"}}) err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
func TestMappingFormFieldNotSent(t *testing.T) {
var s struct {
F string `form:"field,default=defVal"`
}
err := mapForm(&s, map[string][]string{})
require.NoError(t, err)
assert.Equal(t, "defVal", s.F)
}
func TestMappingFormWithEmptyToDefault(t *testing.T) {
var s struct {
F string `form:"field,default=DefVal"`
}
err := mapForm(&s, map[string][]string{"field": {""}})
require.NoError(t, err)
assert.Equal(t, "DefVal", s.F)
}
func TestMapFormWithTag(t *testing.T) { func TestMapFormWithTag(t *testing.T) {
var s struct { var s struct {
F int `externalTag:"field"` F int `externalTag:"field"`
} }
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag") err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 6, s.F) assert.Equal(t, 6, s.F)
} }
@ -170,7 +192,7 @@ func TestMappingTime(t *testing.T) {
var err error var err error
time.Local, err = time.LoadLocation("Europe/Berlin") time.Local, err = time.LoadLocation("Europe/Berlin")
assert.NoError(t, err) require.NoError(t, err)
err = mapForm(&s, map[string][]string{ err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"}, "Time": {"2019-01-20T16:02:58Z"},
@ -179,7 +201,7 @@ func TestMappingTime(t *testing.T) {
"CSTTime": {"2019-01-20"}, "CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"}, "UTCTime": {"2019-01-20"},
}) })
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String()) assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String()) assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
@ -194,29 +216,68 @@ func TestMappingTime(t *testing.T) {
Time time.Time `time_location:"wrong"` Time time.Time `time_location:"wrong"`
} }
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}}) err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
assert.Error(t, err) require.Error(t, err)
// wrong time value // wrong time value
var wrongTime struct { var wrongTime struct {
Time time.Time Time time.Time
} }
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}}) err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
assert.Error(t, err) require.Error(t, err)
}
type bindTestData struct {
need any
got any
in map[string][]string
}
func TestMappingTimeUnixNano(t *testing.T) {
type needFixUnixNanoEmpty struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
}
// ok
tests := []bindTestData{
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}},
{need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
} }
func TestMappingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
type needFixDurationEmpty struct {
Duration time.Duration `form:"duration"`
}
var s struct { var s struct {
D time.Duration D time.Duration
} }
// ok // ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form") err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D) assert.Equal(t, 5*time.Second, s.D)
// ok
tests := []bindTestData{
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}},
{need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}},
}
for _, v := range tests {
err := mapForm(v.got, v.in)
require.NoError(t, err)
assert.Equal(t, v.need, v.got)
}
// error // error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
assert.Error(t, err) require.Error(t, err)
} }
func TestMappingSlice(t *testing.T) { func TestMappingSlice(t *testing.T) {
@ -226,17 +287,17 @@ func TestMappingSlice(t *testing.T) {
// default value // default value
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice) assert.Equal(t, []int{9}, s.Slice)
// ok // ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice) assert.Equal(t, []int{3, 4}, s.Slice)
// error // error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
assert.Error(t, err) require.Error(t, err)
} }
func TestMappingArray(t *testing.T) { func TestMappingArray(t *testing.T) {
@ -246,20 +307,125 @@ func TestMappingArray(t *testing.T) {
// wrong default // wrong default
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.Error(t, err) require.Error(t, err)
// ok // ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array) assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals // error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form") err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
assert.Error(t, err) require.Error(t, err)
// error - wrong value // error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form") err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
assert.Error(t, err) require.Error(t, err)
}
func TestMappingCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{
"slice_multi": {"1", "2"},
"slice_csv": {"1,2"},
"slice_ssv": {"1 2"},
"slice_tsv": {"1 2"},
"slice_pipes": {"1|2"},
"array_multi": {"1", "2"},
"array_csv": {"1,2"},
"array_ssv": {"1 2"},
"array_tsv": {"1 2"},
"array_pipes": {"1|2"},
}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2}, s.SliceMulti)
assert.Equal(t, []int{1, 2}, s.SliceCsv)
assert.Equal(t, []int{1, 2}, s.SliceSsv)
assert.Equal(t, []int{1, 2}, s.SliceTsv)
assert.Equal(t, []int{1, 2}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
}
func TestMappingCollectionFormatInvalid(t *testing.T) {
var s struct {
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
}
err := mappingByPtr(&s, formSource{
"slice_csv": {"1,2"},
}, "form")
require.Error(t, err)
var s2 struct {
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
}
err = mappingByPtr(&s2, formSource{
"array_csv": {"1,2"},
}, "form")
require.Error(t, err)
}
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
}
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)
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
} }
func TestMappingStructField(t *testing.T) { func TestMappingStructField(t *testing.T) {
@ -270,7 +436,7 @@ func TestMappingStructField(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form") err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 9, s.J.I) assert.Equal(t, 9, s.J.I)
} }
@ -288,20 +454,20 @@ func TestMappingPtrField(t *testing.T) {
// With 0 items. // With 0 items.
var req0 ptrRequest var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form") err = mappingByPtr(&req0, formSource{}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Empty(t, req0.Items) assert.Empty(t, req0.Items)
// With 1 item. // With 1 item.
var req1 ptrRequest var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form") err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Len(t, req1.Items, 1) assert.Len(t, req1.Items, 1)
assert.EqualValues(t, 1, req1.Items[0].Key) assert.EqualValues(t, 1, req1.Items[0].Key)
// With 2 items. // With 2 items.
var req2 ptrRequest var req2 ptrRequest
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form") err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Len(t, req2.Items, 2) assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key) assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key) assert.EqualValues(t, 2, req2.Items[1].Key)
@ -313,7 +479,7 @@ func TestMappingMapField(t *testing.T) {
} }
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form") err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M) assert.Equal(t, map[string]int{"one": 1}, s.M)
} }
@ -324,7 +490,7 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
var s S var s S
err := mappingByPtr(&s, formSource{}, "form") err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err) require.NoError(t, err)
} }
type customUnmarshalParamHex int type customUnmarshalParamHex int
@ -343,7 +509,7 @@ func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
Foo customUnmarshalParamHex `form:"foo"` Foo customUnmarshalParamHex `form:"foo"`
} }
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form") err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, 245, s.Foo) assert.EqualValues(t, 245, s.Foo)
} }
@ -353,7 +519,7 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
Foo customUnmarshalParamHex `uri:"foo"` Foo customUnmarshalParamHex `uri:"foo"`
} }
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri") err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, 245, s.Foo) assert.EqualValues(t, 245, s.Foo)
} }
@ -367,7 +533,7 @@ type customUnmarshalParamType struct {
func (f *customUnmarshalParamType) UnmarshalParam(param string) error { func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
parts := strings.Split(param, ":") parts := strings.Split(param, ":")
if len(parts) != 3 { if len(parts) != 3 {
return fmt.Errorf("invalid format") return errors.New("invalid format")
} }
f.Protocol = parts[0] f.Protocol = parts[0]
f.Path = parts[1] f.Path = parts[1]
@ -380,11 +546,11 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
FileData customUnmarshalParamType `form:"data"` FileData customUnmarshalParamType `form:"data"`
} }
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.Equal(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path) assert.Equal(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name) assert.Equal(t, "happiness", s.FileData.Name)
} }
func TestMappingCustomStructTypeWithURITag(t *testing.T) { func TestMappingCustomStructTypeWithURITag(t *testing.T) {
@ -392,11 +558,11 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
FileData customUnmarshalParamType `uri:"data"` FileData customUnmarshalParamType `uri:"data"`
} }
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.Equal(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path) assert.Equal(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name) assert.Equal(t, "happiness", s.FileData.Name)
} }
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) { func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
@ -404,11 +570,11 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
FileData *customUnmarshalParamType `form:"data"` FileData *customUnmarshalParamType `form:"data"`
} }
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.Equal(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path) assert.Equal(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name) assert.Equal(t, "happiness", s.FileData.Name)
} }
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) { func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
@ -416,9 +582,175 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
FileData *customUnmarshalParamType `uri:"data"` FileData *customUnmarshalParamType `uri:"data"`
} }
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
assert.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.Equal(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path) assert.Equal(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name) assert.Equal(t, "happiness", s.FileData.Name)
}
type customPath []string
func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
func TestMappingCustomSliceUri(t *testing.T) {
var s struct {
FileData customPath `uri:"path"`
}
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 TestMappingCustomSliceForm(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
}
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])
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param)
if err != nil {
return err
}
*o = oid
return nil
}
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
_, err := hex.Decode(oid[:], []byte(s))
if err != nil {
return nilObjectID, err
}
return oid, nil
}
func TestMappingCustomArrayUri(t *testing.T) {
var s struct {
FileData objectID `uri:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
var s struct {
FileData objectID `form:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.Equal(t, expected, s.FileData)
}
func TestMappingEmptyValues(t *testing.T) {
t.Run("slice with default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present but empty
err = mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{5}, s.Slice)
// field present with values
err = mappingByPtr(&s, formSource{"slice": {"1", "2", "3"}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.Slice)
})
t.Run("array with default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array,default=5"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
// field present but empty
err = mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{5}, s.Array)
})
t.Run("slice without default", func(t *testing.T) {
var s struct {
Slice []int `form:"slice"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"slice": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int(nil), s.Slice)
})
t.Run("array without default", func(t *testing.T) {
var s struct {
Array [1]int `form:"array"`
}
// field present but empty
err := mappingByPtr(&s, formSource{"array": {}}, "form")
require.NoError(t, err)
assert.Equal(t, [1]int{0}, s.Array)
})
t.Run("slice with collection format", func(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi,default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:"slice_csv,default=1;2;3" collection_format:"csv"`
}
// field not present
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
// field present but empty
err = mappingByPtr(&s, formSource{"slice_multi": {}, "slice_csv": {}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
})
} }

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestFormMultipartBindingBindOneFile(t *testing.T) { func TestFormMultipartBindingBindOneFile(t *testing.T) {
@ -27,7 +28,7 @@ func TestFormMultipartBindingBindOneFile(t *testing.T) {
req := createRequestMultipartFiles(t, file) req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
assert.NoError(t, err) require.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file) assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file) assertMultipartFileHeader(t, s.FilePtr, file)
@ -53,7 +54,7 @@ func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s) err := FormMultipart.Bind(req, &s)
assert.NoError(t, err) require.NoError(t, err)
assert.Len(t, s.SliceValues, len(files)) assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files)) assert.Len(t, s.SlicePtrs, len(files))
@ -90,7 +91,7 @@ func TestFormMultipartBindingBindError(t *testing.T) {
} { } {
req := createRequestMultipartFiles(t, files...) req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s) err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err) require.Error(t, err)
} }
} }
@ -106,17 +107,17 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
mw := multipart.NewWriter(&body) mw := multipart.NewWriter(&body)
for _, file := range files { for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err) require.NoError(t, err)
n, err := fw.Write(file.Content) n, err := fw.Write(file.Content)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, len(file.Content), n) assert.Equal(t, len(file.Content), n)
} }
err := mw.Close() err := mw.Close()
assert.NoError(t, err) require.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body) req, err := http.NewRequest(http.MethodPost, "/", &body)
assert.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req return req
@ -127,12 +128,12 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test
assert.Equal(t, int64(len(file.Content)), fh.Size) assert.Equal(t, int64(len(file.Content)), fh.Size)
fl, err := fh.Open() fl, err := fh.Open()
assert.NoError(t, err) require.NoError(t, err)
body, err := io.ReadAll(fl) body, err := io.ReadAll(fl)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, string(file.Content), string(body)) assert.Equal(t, string(file.Content), string(body))
err = fl.Close() err = fl.Close()
assert.NoError(t, err) require.NoError(t, err)
} }

56
binding/plain.go Normal file
View File

@ -0,0 +1,56 @@
package binding
import (
"fmt"
"io"
"net/http"
"reflect"
"github.com/gin-gonic/gin/internal/bytesconv"
)
type plainBinding struct{}
func (plainBinding) Name() string {
return "plain"
}
func (plainBinding) Bind(req *http.Request, obj any) error {
all, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return decodePlain(all, obj)
}
func (plainBinding) BindBody(body []byte, obj any) error {
return decodePlain(body, obj)
}
func decodePlain(data []byte, obj any) error {
if obj == nil {
return nil
}
v := reflect.ValueOf(obj)
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() == reflect.String {
v.SetString(bytesconv.BytesToString(data))
return nil
}
if _, ok := v.Interface().([]byte); ok {
v.SetBytes(data)
return nil
}
return fmt.Errorf("type (%T) unknown type", v)
}

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type testInterface interface { type testInterface interface {
@ -113,10 +114,10 @@ func TestValidateNoValidationValues(t *testing.T) {
test := createNoValidationValues() test := createNoValidationValues()
empty := structNoValidationValues{} empty := structNoValidationValues{}
assert.Nil(t, validate(test)) require.NoError(t, validate(test))
assert.Nil(t, validate(&test)) require.NoError(t, validate(&test))
assert.Nil(t, validate(empty)) require.NoError(t, validate(empty))
assert.Nil(t, validate(&empty)) require.NoError(t, validate(&empty))
assert.Equal(t, origin, test) assert.Equal(t, origin, test)
} }
@ -157,38 +158,38 @@ type structNoValidationPointer struct {
} }
func TestValidateNoValidationPointers(t *testing.T) { func TestValidateNoValidationPointers(t *testing.T) {
//origin := createNoValidation_values() // origin := createNoValidation_values()
//test := createNoValidation_values() // test := createNoValidation_values()
empty := structNoValidationPointer{} empty := structNoValidationPointer{}
//assert.Nil(t, validate(test)) // assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test)) // assert.Nil(t, validate(&test))
assert.Nil(t, validate(empty)) require.NoError(t, validate(empty))
assert.Nil(t, validate(&empty)) require.NoError(t, validate(&empty))
//assert.Equal(t, origin, test) // assert.Equal(t, origin, test)
} }
type Object map[string]any type Object map[string]any
func TestValidatePrimitives(t *testing.T) { func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj)) require.NoError(t, validate(obj))
assert.NoError(t, validate(&obj)) require.NoError(t, validate(&obj))
assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj) assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2)) require.NoError(t, validate(obj2))
assert.NoError(t, validate(&obj2)) require.NoError(t, validate(&obj2))
nu := 10 nu := 10
assert.NoError(t, validate(nu)) require.NoError(t, validate(nu))
assert.NoError(t, validate(&nu)) require.NoError(t, validate(&nu))
assert.Equal(t, 10, nu) assert.Equal(t, 10, nu)
str := "value" str := "value"
assert.NoError(t, validate(str)) require.NoError(t, validate(str))
assert.NoError(t, validate(&str)) require.NoError(t, validate(&str))
assert.Equal(t, "value", str) assert.Equal(t, "value", str)
} }
@ -197,7 +198,7 @@ type structModifyValidation struct {
} }
func toZero(sl validator.StructLevel) { func toZero(sl validator.StructLevel) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation) s := sl.Top().Interface().(*structModifyValidation)
s.Integer = 0 s.Integer = 0
} }
@ -212,8 +213,8 @@ func TestValidateAndModifyStruct(t *testing.T) {
s := structModifyValidation{Integer: 1} s := structModifyValidation{Integer: 1}
errs := validate(&s) errs := validate(&s)
assert.Nil(t, errs) require.NoError(t, errs)
assert.Equal(t, s, structModifyValidation{Integer: 0}) assert.Equal(t, structModifyValidation{Integer: 0}, s)
} }
// structCustomValidation is a helper struct we use to check that // structCustomValidation is a helper struct we use to check that
@ -239,14 +240,14 @@ func TestValidatorEngine(t *testing.T) {
err := engine.RegisterValidation("notone", notOne) err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error // Check that we can register custom validation without error
assert.Nil(t, err) require.NoError(t, err)
// Create an instance which will fail validation // Create an instance which will fail validation
withOne := structCustomValidation{Integer: 1} withOne := structCustomValidation{Integer: 1}
errs := validate(withOne) errs := validate(withOne)
// Check that we got back non-nil errs // Check that we got back non-nil errs
assert.NotNil(t, errs) require.Error(t, errs)
// Check that the error matches expectation // Check that the error matches expectation
assert.Error(t, errs, "", "", "notone") require.Error(t, errs, "notone")
} }

View File

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

View File

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

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

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

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

@ -0,0 +1,42 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go_json
package json
import (
"io"
"github.com/goccy/go-json"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/goccy/go-json"
func init() {
API = gojsonApi{}
}
type gojsonApi struct{}
func (j gojsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j gojsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

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

@ -0,0 +1,41 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
package json
import (
"encoding/json"
"io"
)
// Package indicates what library is being used for JSON encoding.
const Package = "encoding/json"
func init() {
API = jsonApi{}
}
type jsonApi struct{}
func (j jsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

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

@ -0,0 +1,44 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build jsoniter
package json
import (
"io"
jsoniter "github.com/json-iterator/go"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/json-iterator/go"
func init() {
API = jsoniterApi{}
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type jsoniterApi struct{}
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

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

@ -0,0 +1,44 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build sonic && (linux || windows || darwin)
package json
import (
"io"
"github.com/bytedance/sonic"
)
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/bytedance/sonic"
func init() {
API = sonicApi{}
}
var json = sonic.ConfigStd
type sonicApi struct{}
func (j sonicApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j sonicApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -6,8 +6,11 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"io/fs"
"log" "log"
"maps"
"math" "math"
"mime/multipart" "mime/multipart"
"net" "net"
@ -34,7 +37,9 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
MIMETOML = binding.MIMETOML MIMETOML = binding.MIMETOML
MIMEPROTOBUF = binding.MIMEPROTOBUF
) )
// BodyBytesKey indicates a default body bytes key. // BodyBytesKey indicates a default body bytes key.
@ -70,7 +75,7 @@ type Context struct {
mu sync.RWMutex mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request. // Keys is a key/value pair exclusively for the context of each request.
Keys map[string]any Keys map[any]any
// Errors is a list of errors attached to all the handlers/middlewares who used this context. // Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs Errors errorMsgs
@ -127,11 +132,8 @@ func (c *Context) Copy() *Context {
cp.fullPath = c.fullPath cp.fullPath = c.fullPath
cKeys := c.Keys cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
c.mu.RLock() c.mu.RLock()
for k, v := range cKeys { cp.Keys = maps.Clone(cKeys)
cp.Keys[k] = v
}
c.mu.RUnlock() c.mu.RUnlock()
cParams := c.Params cParams := c.Params
@ -152,6 +154,9 @@ func (c *Context) HandlerName() string {
func (c *Context) HandlerNames() []string { func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers)) hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers { for _, val := range c.handlers {
if val == nil {
continue
}
hn = append(hn, nameOfFunction(val)) hn = append(hn, nameOfFunction(val))
} }
return hn return hn
@ -181,8 +186,10 @@ func (c *Context) FullPath() string {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for c.index < int8(len(c.handlers)) { for c.index < safeInt8(len(c.handlers)) {
c.handlers[c.index](c) if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
c.index++ c.index++
} }
} }
@ -208,6 +215,14 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort() c.Abort()
} }
// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
// This method stops the chain, writes the status code and return a JSON body without escaping.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
c.Abort()
c.PureJSON(code, jsonObj)
}
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. // AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body. // This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
@ -256,12 +271,12 @@ func (c *Context) Error(err error) *Error {
/************************************/ /************************************/
// Set is used to store a new key/value pair exclusively for this context. // Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) { func (c *Context) Set(key any, value any) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]any) c.Keys = make(map[any]any)
} }
c.Keys[key] = value c.Keys[key] = value
@ -269,7 +284,7 @@ func (c *Context) Set(key string, value any) {
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false) // If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) { func (c *Context) Get(key any) (value any, exists bool) {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
value, exists = c.Keys[key] value, exists = c.Keys[key]
@ -277,115 +292,188 @@ func (c *Context) Get(key string) (value any, exists bool) {
} }
// MustGet returns the value for the given key if it exists, otherwise it panics. // MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) any { func (c *Context) MustGet(key any) any {
if value, exists := c.Get(key); exists { if value, exists := c.Get(key); exists {
return value return value
} }
panic("Key \"" + key + "\" does not exist") panic(fmt.Sprintf("key %v does not exist", key))
}
func getTyped[T any](c *Context, key any) (res T) {
if val, ok := c.Get(key); ok && val != nil {
res, _ = val.(T)
}
return
} }
// GetString returns the value associated with the key as a string. // GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) { func (c *Context) GetString(key any) string {
if val, ok := c.Get(key); ok && val != nil { return getTyped[string](c, key)
s, _ = val.(string)
}
return
} }
// GetBool returns the value associated with the key as a boolean. // GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) { func (c *Context) GetBool(key any) bool {
if val, ok := c.Get(key); ok && val != nil { return getTyped[bool](c, key)
b, _ = val.(bool)
}
return
} }
// GetInt returns the value associated with the key as an integer. // GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) { func (c *Context) GetInt(key any) int {
if val, ok := c.Get(key); ok && val != nil { return getTyped[int](c, key)
i, _ = val.(int)
}
return
} }
// GetInt64 returns the value associated with the key as an integer. // GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt64(key string) (i64 int64) { func (c *Context) GetInt8(key any) int8 {
if val, ok := c.Get(key); ok && val != nil { return getTyped[int8](c, key)
i64, _ = val.(int64) }
}
return // GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key any) int16 {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key any) int32 {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key any) int64 {
return getTyped[int64](c, key)
} }
// GetUint returns the value associated with the key as an unsigned integer. // GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) { func (c *Context) GetUint(key any) uint {
if val, ok := c.Get(key); ok && val != nil { return getTyped[uint](c, key)
ui, _ = val.(uint)
}
return
} }
// GetUint64 returns the value associated with the key as an unsigned integer. // GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint64(key string) (ui64 uint64) { func (c *Context) GetUint8(key any) uint8 {
if val, ok := c.Get(key); ok && val != nil { return getTyped[uint8](c, key)
ui64, _ = val.(uint64) }
}
return // GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key any) uint16 {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key any) uint32 {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key any) uint64 {
return getTyped[uint64](c, key)
}
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key any) float32 {
return getTyped[float32](c, key)
} }
// GetFloat64 returns the value associated with the key as a float64. // GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) { func (c *Context) GetFloat64(key any) float64 {
if val, ok := c.Get(key); ok && val != nil { return getTyped[float64](c, key)
f64, _ = val.(float64)
}
return
} }
// GetTime returns the value associated with the key as time. // GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) { func (c *Context) GetTime(key any) time.Time {
if val, ok := c.Get(key); ok && val != nil { return getTyped[time.Time](c, key)
t, _ = val.(time.Time)
}
return
} }
// GetDuration returns the value associated with the key as a duration. // GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) { func (c *Context) GetDuration(key any) time.Duration {
if val, ok := c.Get(key); ok && val != nil { return getTyped[time.Duration](c, key)
d, _ = val.(time.Duration) }
}
return // GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key)
}
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key any) []int8 {
return getTyped[[]int8](c, key)
}
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key any) []int16 {
return getTyped[[]int16](c, key)
}
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key any) []int32 {
return getTyped[[]int32](c, key)
}
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key any) []int64 {
return getTyped[[]int64](c, key)
}
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key any) []uint {
return getTyped[[]uint](c, key)
}
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key any) []uint8 {
return getTyped[[]uint8](c, key)
}
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key any) []uint16 {
return getTyped[[]uint16](c, key)
}
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key any) []uint32 {
return getTyped[[]uint32](c, key)
}
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key any) []uint64 {
return getTyped[[]uint64](c, key)
}
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key any) []float32 {
return getTyped[[]float32](c, key)
}
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key any) []float64 {
return getTyped[[]float64](c, key)
} }
// GetStringSlice returns the value associated with the key as a slice of strings. // GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) { func (c *Context) GetStringSlice(key any) []string {
if val, ok := c.Get(key); ok && val != nil { return getTyped[[]string](c, key)
ss, _ = val.([]string)
}
return
} }
// GetStringMap returns the value associated with the key as a map of interfaces. // GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]any) { func (c *Context) GetStringMap(key any) map[string]any {
if val, ok := c.Get(key); ok && val != nil { return getTyped[map[string]any](c, key)
sm, _ = val.(map[string]any)
}
return
} }
// GetStringMapString returns the value associated with the key as a map of strings. // GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) { func (c *Context) GetStringMapString(key any) map[string]string {
if val, ok := c.Get(key); ok && val != nil { return getTyped[map[string]string](c, key)
sms, _ = val.(map[string]string)
}
return
} }
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) { func (c *Context) GetStringMapStringSlice(key any) map[string][]string {
if val, ok := c.Get(key); ok && val != nil { return getTyped[map[string][]string](c, key)
smss, _ = val.(map[string][]string) }
// Delete deletes the key from the Context's Key map, if it exists.
// This operation is safe to be used by concurrent go-routines
func (c *Context) Delete(key any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys != nil {
delete(c.Keys, key)
} }
return
} }
/************************************/ /************************************/
@ -468,7 +556,7 @@ func (c *Context) QueryArray(key string) (values []string) {
func (c *Context) initQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { if c.queryCache == nil {
if c.Request != nil { if c.Request != nil && c.Request.URL != nil {
c.queryCache = c.Request.URL.Query() c.queryCache = c.Request.URL.Query()
} else { } else {
c.queryCache = url.Values{} c.queryCache = url.Values{}
@ -494,7 +582,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) { func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.initQueryCache() c.initQueryCache()
return c.get(c.queryCache, key) return getMapFromFormData(c.queryCache, key)
} }
// PostForm returns the specified key from a POST urlencoded form or multipart form // PostForm returns the specified key from a POST urlencoded form or multipart form
@ -567,22 +655,32 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.initFormCache() c.initFormCache()
return c.get(c.formCache, key) return getMapFromFormData(c.formCache, key)
} }
// get is an internal method and returns a map which satisfies conditions. // getMapFromFormData return a map which satisfies conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) { // It parses from data with bracket notation like "key[subkey]=value" into a map.
dicts := make(map[string]string) func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
exist := false d := make(map[string]string)
found := false
keyLen := len(key)
for k, v := range m { for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key { if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 { continue
exist = true }
dicts[k[i+1:][:j]] = v[0]
} if k[:keyLen] != key || k[keyLen] != '[' {
continue
}
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
found = true
d[k[keyLen+1:keyLen+1+j]] = v[0]
} }
} }
return dicts, exist
return d, found
} }
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.
@ -607,14 +705,22 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
} }
// SaveUploadedFile uploads the form file to specific dst. // SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error { func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
src, err := file.Open() src, err := file.Open()
if err != nil { if err != nil {
return err return err
} }
defer src.Close() defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil { var mode os.FileMode = 0o750
if len(perm) > 0 {
mode = perm[0]
}
dir := filepath.Dir(dst)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err return err
} }
@ -667,6 +773,11 @@ func (c *Context) BindTOML(obj any) error {
return c.MustBindWith(obj, binding.TOML) return c.MustBindWith(obj, binding.TOML)
} }
// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
func (c *Context) BindPlain(obj any) error {
return c.MustBindWith(obj, binding.Plain)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj any) error { func (c *Context) BindHeader(obj any) error {
return c.MustBindWith(obj, binding.Header) return c.MustBindWith(obj, binding.Header)
@ -686,8 +797,19 @@ func (c *Context) BindUri(obj any) error {
// It will abort the request with HTTP 400 if any error occurs. // It will abort the request with HTTP 400 if any error occurs.
// See the binding package. // See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error { func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil { err := c.ShouldBindWith(obj, b)
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck if err != nil {
var maxBytesErr *http.MaxBytesError
// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
// https://github.com/goccy/go-json/issues/485
// https://github.com/bytedance/sonic/issues/800
switch {
case errors.As(err, &maxBytesErr):
c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
default:
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
}
return err return err
} }
return nil return nil
@ -708,36 +830,71 @@ func (c *Context) ShouldBind(obj any) error {
} }
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). // ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
//
// Example:
//
// POST /user
// Content-Type: application/json
//
// Request Body:
// {
// "name": "Manu",
// "age": 20
// }
//
// type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// var user User
// if err := c.ShouldBindJSON(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
// }
// c.JSON(http.StatusOK, user)
func (c *Context) ShouldBindJSON(obj any) error { func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON) return c.ShouldBindWith(obj, binding.JSON)
} }
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error { func (c *Context) ShouldBindXML(obj any) error {
return c.ShouldBindWith(obj, binding.XML) return c.ShouldBindWith(obj, binding.XML)
} }
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
// It works like ShouldBindJSON but binds query parameters from the URL.
func (c *Context) ShouldBindQuery(obj any) error { func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query) return c.ShouldBindWith(obj, binding.Query)
} }
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). // ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error { func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML) return c.ShouldBindWith(obj, binding.YAML)
} }
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). // ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error { func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML) return c.ShouldBindWith(obj, binding.TOML)
} }
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// 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). // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
// It works like ShouldBindJSON but binds values from HTTP headers.
func (c *Context) ShouldBindHeader(obj any) error { func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header) return c.ShouldBindWith(obj, binding.Header)
} }
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
// It works like ShouldBindJSON but binds parameters from the URI.
func (c *Context) ShouldBindUri(obj any) error { func (c *Context) ShouldBindUri(obj any) error {
m := make(map[string][]string, len(c.Params)) m := make(map[string][]string, len(c.Params))
for _, v := range c.Params { for _, v := range c.Params {
@ -794,9 +951,14 @@ func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML) return c.ShouldBindBodyWith(obj, binding.TOML)
} }
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
return c.ShouldBindBodyWith(obj, binding.Plain)
}
// ClientIP implements one best effort algorithm to return the real client IP. // ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. // It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]). // If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy, // If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned. // the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
@ -934,6 +1096,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
}) })
} }
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
func (c *Context) SetCookieData(cookie *http.Cookie) {
if cookie.Path == "" {
cookie.Path = "/"
}
if cookie.SameSite == http.SameSiteDefaultMode {
cookie.SameSite = c.sameSite
}
http.SetCookie(c.Writer, cookie)
}
// Cookie returns the named cookie provided in the request or // Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found. And return the named cookie is unescaped. // ErrNoCookie if not found. And return the named cookie is unescaped.
// If multiple cookies match the given name, only one cookie will // If multiple cookies match the given name, only one cookie will
@ -1136,14 +1311,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
// Negotiate contains all negotiations data. // Negotiate contains all negotiations data.
type Negotiate struct { type Negotiate struct {
Offered []string Offered []string
HTMLName string HTMLName string
HTMLData any HTMLData any
JSONData any JSONData any
XMLData any XMLData any
YAMLData any YAMLData any
Data any Data any
TOMLData any TOMLData any
PROTOBUFData any
} }
// Negotiate calls different Render according to acceptable Accept format. // Negotiate calls different Render according to acceptable Accept format.
@ -1161,7 +1337,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML: case binding.MIMEYAML, binding.MIMEYAML2:
data := chooseData(config.YAMLData, config.Data) data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data) c.YAML(code, data)
@ -1169,6 +1345,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.TOMLData, config.Data) data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data) c.TOML(code, data)
case binding.MIMEPROTOBUF:
data := chooseData(config.PROTOBUFData, config.Data)
c.ProtoBuf(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
} }

View File

@ -1,37 +0,0 @@
// Copyright 2021 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.19
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed18(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
defer func(mw *multipart.Writer) {
err := mw.Close()
if err != nil {
assert.Error(t, err)
}
}(mw)
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
assert.Panics(t, func() {
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
})
}

View File

@ -1,30 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go1.19
package gin
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestContextFormFileFailed19(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}

35
context_file_test.go Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -10,19 +10,16 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"runtime"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) { func TestIsDebugging(t *testing.T) {
SetMode(DebugMode) SetMode(DebugMode)
assert.True(t, IsDebugging()) assert.True(t, IsDebugging())
@ -46,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re) assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
} }
func TestDebugPrintFunc(t *testing.T) {
DebugPrintFunc = func(format string, values ...any) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrint("debug print func test: %d", 123)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
}
func TestDebugPrintError(t *testing.T) { func TestDebugPrintError(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
@ -59,7 +68,7 @@ func TestDebugPrintError(t *testing.T) {
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -71,7 +80,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
} }
re := captureOutput(t, func() { re := captureOutput(t, func() {
SetMode(DebugMode) SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest}) debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode) SetMode(TestMode)
}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
@ -102,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
SetMode(TestMode) SetMode(TestMode)
}) })
m, e := getMinVer(runtime.Version()) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
if e == nil && m < ginSupportMinGoVer { }
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.18+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) runtimeVersion = "go1.23.12"
} re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }
func TestDebugPrintWARNINGNew(t *testing.T) { func TestDebugPrintWARNINGNew(t *testing.T) {
@ -154,13 +168,13 @@ func TestGetMinVer(t *testing.T) {
var m uint64 var m uint64
var e error var e error
_, e = getMinVer("go1") _, e = getMinVer("go1")
assert.NotNil(t, e) require.Error(t, e)
m, e = getMinVer("go1.1") m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
assert.Nil(t, e) require.NoError(t, e)
m, e = getMinVer("go1.1.1") m, e = getMinVer("go1.1.1")
assert.Nil(t, e) require.NoError(t, e)
assert.Equal(t, uint64(1), m) assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1") _, e = getMinVer("go1.1.1.1")
assert.NotNil(t, e) require.Error(t, e)
} }

View File

@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`

16
doc.go
View File

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

View File

@ -26,6 +26,8 @@
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind default value if none provided](#bind-default-value-if-none-provided)
- [Collection format for arrays](#collection-format-for-arrays)
- [Bind Uri](#bind-uri) - [Bind Uri](#bind-uri)
- [Bind custom unmarshaler](#bind-custom-unmarshaler) - [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header) - [Bind Header](#bind-header)
@ -61,6 +63,7 @@
- [http2 server push](#http2-server-push) - [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie) - [Set and get a cookie](#set-and-get-a-cookie)
- [Custom json codec at runtime](#custom-json-codec-at-runtime)
- [Don't trust all proxies](#dont-trust-all-proxies) - [Don't trust all proxies](#dont-trust-all-proxies)
- [Testing](#testing) - [Testing](#testing)
@ -68,7 +71,7 @@
### Build with json replacement ### Build with json replacement
Gin uses `encoding/json` as default json package but you can change it by build from other tags. Gin uses `encoding/json` as the default JSON package but you can change it by building from other tags.
[jsoniter](https://github.com/json-iterator/go) [jsoniter](https://github.com/json-iterator/go)
@ -82,10 +85,10 @@ go build -tags=jsoniter .
go build -tags=go_json . go build -tags=go_json .
``` ```
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu support avx instruction.) [sonic](https://github.com/bytedance/sonic)
```sh ```sh
$ go build -tags="sonic avx" . $ go build -tags=sonic .
``` ```
### Build without `MsgPack` rendering feature ### Build without `MsgPack` rendering feature
@ -118,7 +121,7 @@ func main() {
router.HEAD("/someHead", head) router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options) router.OPTIONS("/someOptions", options)
// By default it serves on :8080 unless a // By default, it serves on :8080 unless a
// PORT environment variable was defined. // PORT environment variable was defined.
router.Run() router.Run()
// router.Run(":3000") for a hard coded port // router.Run(":3000") for a hard coded port
@ -170,7 +173,7 @@ func main() {
router := gin.Default() router := gin.Default()
// Query string parameters are parsed using the existing underlying request object. // Query string parameters are parsed using the existing underlying request object.
// The request responds to an url matching: /welcome?firstname=Jane&lastname=Doe // The request responds to a URL matching: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) { router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest") firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
@ -298,7 +301,7 @@ curl -X POST http://localhost:8080/upload \
#### Multiple files #### Multiple files
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple). See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
```go ```go
func main() { func main() {
@ -338,16 +341,16 @@ func main() {
router := gin.Default() router := gin.Default()
// Simple group: v1 // Simple group: v1
v1 := router.Group("/v1")
{ {
v1 := router.Group("/v1")
v1.POST("/login", loginEndpoint) v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint) v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint) v1.POST("/read", readEndpoint)
} }
// Simple group: v2 // Simple group: v2
v2 := router.Group("/v2")
{ {
v2 := router.Group("/v2")
v2.POST("/login", loginEndpoint) v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint) v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint) v2.POST("/read", readEndpoint)
@ -524,7 +527,7 @@ func main() {
return c.Writer.Status() < http.StatusInternalServerError return c.Writer.Status() < http.StatusInternalServerError
} }
engine.Use(gin.LoggerWithConfig(loggerConfig)) router.Use(gin.LoggerWithConfig(loggerConfig))
router.Use(gin.Recovery()) router.Use(gin.Recovery())
// skipped // skipped
@ -613,7 +616,7 @@ You can also specify that specific fields are required. If a field is decorated
```go ```go
// Binding from JSON // Binding from JSON
type Login struct { type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"` User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"` Password string `form:"password" json:"password" xml:"password" binding:"required"`
} }
@ -702,7 +705,7 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} {"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
``` ```
Skip validate: when running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again. Skip-validation: Running the example above using the `curl` command returns an error. This is because the example uses `binding:"required"` for `Password`. If instead, you use `binding:"-"` for `Password`, then it will not return an error when you run the example again.
### Custom Validators ### Custom Validators
@ -830,6 +833,8 @@ type Person struct {
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"` CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"` UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
} }
func main() { func main() {
@ -849,6 +854,8 @@ func startPage(c *gin.Context) {
log.Println(person.Birthday) log.Println(person.Birthday)
log.Println(person.CreateTime) log.Println(person.CreateTime)
log.Println(person.UnixTime) log.Println(person.UnixTime)
log.Println(person.UnixMilliTime)
log.Println(person.UnixMicroTime)
} }
c.String(http.StatusOK, "Success") c.String(http.StatusOK, "Success")
@ -858,7 +865,107 @@ func startPage(c *gin.Context) {
Test it with: Test it with:
```sh ```sh
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033" curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
```
### Bind default value if none provided
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
```go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name,default=William"`
Age int `form:"age,default=10"`
Friends []string `form:"friends,default=Will;Bill"`
Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
LapTimes []int `form:"lap_times,default=1;2;3" collection_format:"csv"`
}
func main() {
g := gin.Default()
g.POST("/person", func(c *gin.Context) {
var req Person
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, req)
})
_ = g.Run("localhost:8080")
}
```
```
curl -X POST http://localhost:8080/person
{"Name":"William","Age":10,"Friends":["Will","Bill"],"Colors":["red","blue"],"LapTimes":[1,2,3]}
```
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
#### Collection format for arrays
| Format | Description | Example |
| --------------- | --------------------------------------------------------- | ----------------------- |
| multi (default) | Multiple parameter instances rather than multiple values. | key=foo&key=bar&key=baz |
| csv | Comma-separated values. | foo,bar,baz |
| ssv | Space-separated values. | foo bar baz |
| tsv | Tab-separated values. | "foo\tbar\tbaz" |
| pipes | Pipe-separated values. | foo\|bar\|baz |
```go
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Addresses []string `form:"addresses" collection_format:"csv"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Addresses)
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success")
}
```
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
``` ```
### Bind Uri ### Bind Uri
@ -1081,7 +1188,7 @@ func main() {
}) })
r.GET("/moreJSON", func(c *gin.Context) { r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct // You can also use a struct
var msg struct { var msg struct {
Name string `json:"user"` Name string `json:"user"`
Message string Message string
@ -1150,7 +1257,7 @@ func main() {
#### JSONP #### JSONP
Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists. Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.
```go ```go
func main() { func main() {
@ -1199,7 +1306,7 @@ func main() {
#### PureJSON #### PureJSON
Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead. Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead.
This feature is unavailable in Go 1.6 and lower. This feature is unavailable in Go 1.6 and lower.
```go ```go
@ -1287,13 +1394,19 @@ func main() {
### HTML rendering ### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles() Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go ```go
//go:embed templates/*
var templates embed.FS
func main() { func main() {
router := gin.Default() router := gin.Default()
router.LoadHTMLGlob("templates/*") router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
//or
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) { router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{ c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website", "title": "Main website",
@ -1384,7 +1497,7 @@ You may use custom delims
#### Custom Template Funcs #### Custom Template Funcs
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template). See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/template).
main.go main.go
@ -1436,7 +1549,7 @@ Date: 2017/07/01
### Multitemplate ### Multitemplate
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. Gin allows only one html.Template by default. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects ### Redirects
@ -1985,7 +2098,7 @@ type formB struct {
func SomeHandler(c *gin.Context) { func SomeHandler(c *gin.Context) {
objA := formA{} objA := formA{}
objB := formB{} objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused. // Calling c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil { if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`) c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now. // Always an error is occurred by this because c.Request.Body is EOF now.
@ -2192,12 +2305,64 @@ func main() {
router := gin.Default() router := gin.Default()
router.GET("/cookie", func(c *gin.Context) { router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
You can also use the `SetCookieData` method, which accepts a `*http.Cookie` directly for more flexibility:
```go
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie") cookie, err := c.Cookie("gin_cookie")
if err != nil { if err != nil {
cookie = "NotSet" cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) // Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
} }
fmt.Printf("Cookie value: %s \n", cookie) fmt.Printf("Cookie value: %s \n", cookie)
@ -2207,6 +2372,65 @@ func main() {
} }
``` ```
### Custom json codec at runtime
Gin support custom json serialization and deserialization logic without using compile tags.
1. Define a custom struct implements the `json.Core` interface.
2. Before your engine starts, assign values to `json.API` using the custom struct.
```go
package main
import (
"io"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/codec/json"
jsoniter "github.com/json-iterator/go"
)
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
// implement api.JsonApi
type customJsonApi struct {
}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
func main() {
//Replace the default json api
json.API = customJsonApi{}
//Start your gin engine
router := gin.Default()
router.Run(":8080")
}
```
## Don't trust all proxies ## Don't trust all proxies
Gin lets you specify which headers to hold the real client IP (if any), Gin lets you specify which headers to hold the real client IP (if any),
@ -2218,7 +2442,7 @@ or network CIDRs from where clients which their request headers related to clien
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs. IPv6 CIDRs.
**Attention:** Gin trust all proxies by default if you don't specify a trusted **Attention:** Gin trusts all proxies by default if you don't specify a trusted
proxy using the function above, **this is NOT safe**. At the same time, if you don't proxy using the function above, **this is NOT safe**. At the same time, if you don't
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`, use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
then `Context.ClientIP()` will return the remote address directly to avoid some then `Context.ClientIP()` will return the remote address directly to avoid some

View File

@ -9,7 +9,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/codec/json"
) )
// ErrorType is an unsigned 64-bit error code as defined in the gin spec. // ErrorType is an unsigned 64-bit error code as defined in the gin spec.
@ -77,7 +77,7 @@ func (msg *Error) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (msg *Error) MarshalJSON() ([]byte, error) { func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON()) return json.API.Marshal(msg.JSON())
} }
// Error implements the error interface. // Error implements the error interface.
@ -91,7 +91,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
} }
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap() // Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error { func (msg Error) Unwrap() error {
return msg.Err return msg.Err
} }
@ -157,7 +157,7 @@ func (a errorMsgs) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) { func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON()) return json.API.Marshal(a.JSON())
} }
func (a errorMsgs) String() string { func (a errorMsgs) String() string {

View File

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

53
fs.go
View File

@ -9,37 +9,42 @@ import (
"os" "os"
) )
type onlyFilesFS struct { // OnlyFilesFS implements an http.FileSystem without `Readdir` functionality.
fs http.FileSystem type OnlyFilesFS struct {
FileSystem http.FileSystem
} }
type neuteredReaddirFile struct { // Open passes `Open` to the upstream implementation without `Readdir` functionality.
http.File func (o OnlyFilesFS) Open(name string) (http.File, error) {
} f, err := o.FileSystem.Open(name)
// Dir returns a http.FileSystem that can be used by http.FileServer(). It is used internally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.
func Dir(root string, listDirectory bool) http.FileSystem {
fs := http.Dir(root)
if listDirectory {
return fs
}
return &onlyFilesFS{fs}
}
// Open conforms to http.Filesystem.
func (fs onlyFilesFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return neuteredReaddirFile{f}, nil
return neutralizedReaddirFile{f}, nil
} }
// Readdir overrides the http.File default implementation. // neutralizedReaddirFile wraps http.File with a specific implementation of `Readdir`.
func (f neuteredReaddirFile) Readdir(_ int) ([]os.FileInfo, error) { type neutralizedReaddirFile struct {
http.File
}
// Readdir overrides the http.File default implementation and always returns nil.
func (n neutralizedReaddirFile) Readdir(_ int) ([]os.FileInfo, error) {
// this disables directory listing // this disables directory listing
return nil, nil return nil, nil
} }
// Dir returns an http.FileSystem that can be used by http.FileServer().
// It is used internally in router.Static().
// if listDirectory == true, then it works the same as http.Dir(),
// otherwise it returns a filesystem that prevents http.FileServer() to list the directory files.
func Dir(root string, listDirectory bool) http.FileSystem {
fs := http.Dir(root)
if listDirectory {
return fs
}
return &OnlyFilesFS{FileSystem: fs}
}

72
fs_test.go Normal file
View File

@ -0,0 +1,72 @@
package gin
import (
"errors"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockFileSystem struct {
open func(name string) (http.File, error)
}
func (m *mockFileSystem) Open(name string) (http.File, error) {
return m.open(name)
}
func TestOnlyFilesFS_Open(t *testing.T) {
var testFile *os.File
mockFS := &mockFileSystem{
open: func(name string) (http.File, error) {
return testFile, nil
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.NoError(t, err)
assert.Equal(t, testFile, file.(neutralizedReaddirFile).File)
}
func TestOnlyFilesFS_Open_err(t *testing.T) {
testError := errors.New("mock")
mockFS := &mockFileSystem{
open: func(_ string) (http.File, error) {
return nil, testError
},
}
fs := &OnlyFilesFS{FileSystem: mockFS}
file, err := fs.Open("foo")
require.ErrorIs(t, err, testError)
assert.Nil(t, file)
}
func Test_neuteredReaddirFile_Readdir(t *testing.T) {
n := neutralizedReaddirFile{}
res, err := n.Readdir(0)
require.NoError(t, err)
assert.Nil(t, res)
}
func TestDir_listDirectory(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, true)
assert.Equal(t, http.Dir(testRoot), fs)
}
func TestDir(t *testing.T) {
testRoot := "foo"
fs := Dir(testRoot, false)
assert.Equal(t, &OnlyFilesFS{FileSystem: http.Dir(testRoot)}, fs)
}

168
gin.go
View File

@ -11,17 +11,23 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
filesystem "github.com/gin-gonic/gin/internal/fs"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
const defaultMultipartMemory = 32 << 20 // 32 MB const (
defaultMultipartMemory = 32 << 20 // 32 MB
escapedColon = "\\:"
colon = ":"
backslash = "\\"
)
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
@ -41,9 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
}, },
} }
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
// HandlerFunc defines the handler used by gin middleware as return value. // HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
@ -89,6 +92,10 @@ const (
type Engine struct { type Engine struct {
RouterGroup RouterGroup
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
routeTreesUpdated sync.Once
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a // RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists. // handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the // For example if /foo/ is requested but a route only exists for /foo, the
@ -128,10 +135,16 @@ type Engine struct {
AppEngine bool AppEngine bool
// UseRawPath if enabled, the url.RawPath will be used to find parameters. // UseRawPath if enabled, the url.RawPath will be used to find parameters.
// The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
// Only use RawPath if you know what you are doing.
UseRawPath bool UseRawPath bool
// UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
// It overrides UseRawPath
UseEscapedPath bool
// UnescapePathValues if true, the path value will be unescaped. // UnescapePathValues if true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true, // If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped. // as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool UnescapePathValues bool
@ -184,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
// - HandleMethodNotAllowed: false // - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false // - UseRawPath: false
// - UseEscapedPath: false
// - UnescapePathValues: true // - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine { func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
@ -201,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform, TrustedPlatform: defaultPlatform,
UseRawPath: false, UseRawPath: false,
UseEscapedPath: false,
RemoveExtraSlash: false, RemoveExtraSlash: false,
UnescapePathValues: true, UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory, MaxMultipartMemory: defaultMultipartMemory,
@ -210,7 +225,7 @@ func New(opts ...OptionFunc) *Engine {
trustedProxies: []string{"0.0.0.0/0", "::/0"}, trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs, trustedCIDRs: defaultTrustedCIDRs,
} }
engine.RouterGroup.engine = engine engine.engine = engine
engine.pool.New = func() any { engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams) return engine.allocateContext(engine.maxParams)
} }
@ -280,6 +295,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
filesystem.FileSystem{FileSystem: fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer. // SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
@ -316,7 +344,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine return engine
} }
// With returns a new Engine instance with the provided options. // With returns an Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine { func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts { for _, opt := range opts {
opt(engine) opt(engine)
@ -358,7 +386,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
} }
// Routes returns a slice of registered routes, including some useful information, such as: // Routes returns a slice of registered routes, including some useful information, such as:
// the http method, path and the handler name. // the http method, path, and the handler name.
func (engine *Engine) Routes() (routes RoutesInfo) { func (engine *Engine) Routes() (routes RoutesInfo) {
for _, tree := range engine.trees { for _, tree := range engine.trees {
routes = iterate("", tree.method, routes, tree.root) routes = iterate("", tree.method, routes, tree.root)
@ -383,23 +411,6 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
return routes return routes
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) { func (engine *Engine) prepareTrustedCIDRs() ([]*net.IPNet, error) {
if engine.trustedProxies == nil { if engine.trustedProxies == nil {
return nil, nil return nil, nil
@ -489,6 +500,26 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false return "", false
} }
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
if n.children == nil {
return
}
for _, child := range n.children {
updateRouteTree(child)
}
}
// updateRouteTrees do update to the route trees
func (engine *Engine) updateRouteTrees() {
for _, tree := range engine.trees {
updateRouteTree(tree.root)
}
}
// parseIP parse a string representation of an IP and returns a net.IP with the // parseIP parse a string representation of an IP and returns a net.IP with the
// minimum byte representation or nil if input is invalid. // minimum byte representation or nil if input is invalid.
func parseIP(ip string) net.IP { func parseIP(ip string) net.IP {
@ -503,6 +534,27 @@ func parseIP(ip string) net.IP {
return parsedIP return parsedIP
} }
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -512,10 +564,14 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
if engine.isUnsafeTrustedProxies() { if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.") "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
} }
err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) server := &http.Server{ // #nosec G112
Addr: addr,
Handler: engine.Handler(),
}
err = server.ListenAndServeTLS(certFile, keyFile)
return return
} }
@ -538,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close() defer listener.Close()
defer os.Remove(file) defer os.Remove(file)
err = http.Serve(listener, engine.Handler()) server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return return
} }
@ -555,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
} }
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
defer f.Close()
listener, err := net.FileListener(f) listener, err := net.FileListener(f)
if err != nil { if err != nil {
return return
@ -564,6 +624,22 @@ func (engine *Engine) RunFd(fd int) (err error) {
return return
} }
// RunQUIC attaches the router to a http.Server and starts listening and serving QUIC requests.
// It is a shortcut for http3.ListenAndServeQUIC(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
debugPrint("Listening and serving QUIC on %s\n", addr)
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests // RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener // through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) { func (engine *Engine) RunListener(listener net.Listener) (err error) {
@ -575,12 +651,19 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
} }
err = http.Serve(listener, engine.Handler()) server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
err = server.Serve(listener)
return return
} }
// ServeHTTP conforms to the http.Handler interface. // ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.routeTreesUpdated.Do(func() {
engine.updateRouteTrees()
})
c := engine.pool.Get().(*Context) c := engine.pool.Get().(*Context)
c.writermem.reset(w) c.writermem.reset(w)
c.Request = req c.Request = req
@ -596,17 +679,23 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal with this, use wisely. // Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index oldIndexValue := c.index
oldHandlers := c.handlers
c.reset() c.reset()
engine.handleHTTPRequest(c) engine.handleHTTPRequest(c)
c.index = oldIndexValue c.index = oldIndexValue
c.handlers = oldHandlers
} }
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method httpMethod := c.Request.Method
rPath := c.Request.URL.Path rPath := c.Request.URL.Path
unescape := false unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
if engine.UseEscapedPath {
rPath = c.Request.URL.EscapedPath()
unescape = engine.UnescapePathValues
} else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
@ -646,7 +735,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
break break
} }
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed && len(t) > 0 {
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response // According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
// containing a list of the target resource's currently supported methods. // containing a list of the target resource's currently supported methods.
allowed := make([]string, 0, len(t)-1) allowed := make([]string, 0, len(t)-1)
@ -693,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
req := c.Request req := c.Request
p := req.URL.Path p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
prefix = regSafePrefix.ReplaceAllString(prefix, "") prefix = sanitizePathChars(prefix)
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/") prefix = removeRepeatedChar(prefix, '/')
p = prefix + "/" + req.URL.Path p = prefix + "/" + req.URL.Path
} }
@ -705,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
redirectRequest(c) redirectRequest(c)
} }
// sanitizePathChars removes unsafe characters from path strings,
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
func sanitizePathChars(s string) string {
return strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
return r
}
return -1
}, s)
}
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request req := c.Request
rPath := req.URL.Path rPath := req.URL.Path

View File

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

246
ginS/gins_test.go Normal file
View File

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

View File

@ -16,18 +16,19 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty) // params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
// params[1]=response status (custom compare status) default:"200 OK" // params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked" // params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) { func testRequest(t *testing.T, params ...string) {
if len(params) == 0 { if len(params) == 0 {
t.Fatal("url cannot be empty") t.Fatal("url cannot be empty")
} }
@ -40,18 +41,18 @@ func testRequest(t *testing.T, params ...string) {
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get(params[0]) resp, err := client.Get(params[0])
assert.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
body, ioerr := io.ReadAll(resp.Body) body, ioerr := io.ReadAll(resp.Body)
assert.NoError(t, ioerr) require.NoError(t, ioerr)
var responseStatus = "200 OK" responseStatus := "200 OK"
if len(params) > 1 && params[1] != "" { if len(params) > 1 && params[1] != "" {
responseStatus = params[1] responseStatus = params[1]
} }
var responseBody = "it worked" responseBody := "it worked"
if len(params) > 2 && params[2] != "" { if len(params) > 2 && params[2] != "" {
responseBody = params[2] responseBody = params[2]
} }
@ -69,17 +70,18 @@ func TestRunEmpty(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":8080")) // Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:8080/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestBadTrustedCIDRs(t *testing.T) { func TestBadTrustedCIDRs(t *testing.T) {
router := New() router := New()
assert.Error(t, router.SetTrustedProxies([]string{"hello/world"})) require.Error(t, router.SetTrustedProxies([]string{"hello/world"}))
} }
/* legacy tests /* legacy tests
@ -87,7 +89,7 @@ func TestBadTrustedCIDRsForRun(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New() router := New()
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.Run(":8080")) require.Error(t, router.Run(":8080"))
} }
func TestBadTrustedCIDRsForRunUnix(t *testing.T) { func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
@ -100,7 +102,7 @@ func TestBadTrustedCIDRsForRunUnix(t *testing.T) {
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunUnix(unixTestSocket)) require.Error(t, router.RunUnix(unixTestSocket))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -112,15 +114,15 @@ func TestBadTrustedCIDRsForRunFd(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err) require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) require.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
assert.NoError(t, err) require.NoError(t, err)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunFd(int(socketFile.Fd()))) require.Error(t, router.RunFd(int(socketFile.Fd())))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -132,12 +134,12 @@ func TestBadTrustedCIDRsForRunListener(t *testing.T) {
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err) require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) require.NoError(t, err)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.Error(t, router.RunListener(listener)) require.Error(t, router.RunListener(listener))
}() }()
// have to wait for the goroutine to start and run the server // have to wait for the goroutine to start and run the server
// otherwise the main thread will complete // otherwise the main thread will complete
@ -148,7 +150,7 @@ func TestBadTrustedCIDRsForRunTLS(t *testing.T) {
os.Setenv("PORT", "") os.Setenv("PORT", "")
router := New() router := New()
router.TrustedProxies = []string{"hello/world"} router.TrustedProxies = []string{"hello/world"}
assert.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) require.Error(t, router.RunTLS(":8080", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
} }
*/ */
@ -164,12 +166,12 @@ func TestRunTLS(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) require.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example") testRequest(t, "https://localhost:8443/example")
} }
func TestPusher(t *testing.T) { func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(` html := template.Must(template.New("https").Parse(`
<html> <html>
<head> <head>
<title>Https Test</title> <title>Https Test</title>
@ -201,7 +203,7 @@ func TestPusher(t *testing.T) {
// otherwise the main thread will complete // otherwise the main thread will complete
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) require.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher") testRequest(t, "https://localhost:8449/pusher")
} }
@ -212,18 +214,19 @@ func TestRunEmptyWithEnv(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run()) assert.NoError(t, router.Run())
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":3123")) // Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:3123/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example") testRequest(t, "http://localhost:3123/example")
} }
func TestRunTooMuchParams(t *testing.T) { func TestRunTooMuchParams(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
assert.NoError(t, router.Run("2", "2")) require.NoError(t, router.Run("2", "2"))
}) })
} }
@ -233,11 +236,12 @@ func TestRunWithPort(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run(":5150")) assert.NoError(t, router.Run(":5150"))
}() }()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.Run(":5150")) // Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:5150/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example") testRequest(t, "http://localhost:5150/example")
} }
@ -257,36 +261,53 @@ func TestUnixSocket(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("unix", unixTestSocket) c, err := net.Dial("unix", unixTestSocket)
assert.NoError(t, err) require.NoError(t, err)
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }
func TestBadUnixSocket(t *testing.T) { func TestBadUnixSocket(t *testing.T) {
router := New() router := New()
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test")) require.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
}
func TestRunQUIC(t *testing.T) {
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
require.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
} }
func TestFileDescriptor(t *testing.T) { func TestFileDescriptor(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err) require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) require.NoError(t, err)
socketFile, err := listener.File() socketFile, err := listener.File()
if isWindows() { if isWindows() {
// not supported by windows, it is unimplemented now // not supported by windows, it is unimplemented now
assert.Error(t, err) require.Error(t, err)
} else { } else {
assert.NoError(t, err) require.NoError(t, err)
} }
if socketFile == nil { if socketFile == nil {
@ -302,29 +323,30 @@ func TestFileDescriptor(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err) require.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }
func TestBadFileDescriptor(t *testing.T) { func TestBadFileDescriptor(t *testing.T) {
router := New() router := New()
assert.Error(t, router.RunFd(0)) require.Error(t, router.RunFd(0))
} }
func TestListener(t *testing.T) { func TestListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0") addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err) require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) require.NoError(t, err)
go func() { go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener)) assert.NoError(t, router.RunListener(listener))
@ -334,14 +356,15 @@ func TestListener(t *testing.T) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String()) c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err) require.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
var response string var responseBuilder strings.Builder
for scanner.Scan() { for scanner.Scan() {
response += scanner.Text() responseBuilder.WriteString(scanner.Text())
} }
response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match") assert.Contains(t, response, "it worked", "resp body should match")
} }
@ -349,11 +372,11 @@ func TestListener(t *testing.T) {
func TestBadListener(t *testing.T) { func TestBadListener(t *testing.T) {
router := New() router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086") addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err) require.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr) listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err) require.NoError(t, err)
listener.Close() listener.Close()
assert.Error(t, router.RunListener(listener)) require.Error(t, router.RunListener(listener))
} }
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
@ -379,7 +402,14 @@ func TestConcurrentHandleContext(t *testing.T) {
wg.Add(iterations) wg.Add(iterations)
for i := 0; i < iterations; i++ { for i := 0; i < iterations; i++ {
go func() { go func() {
testGetRequestHandler(t, router, "/") req, err := http.NewRequest(http.MethodGet, "/", nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
wg.Done() wg.Done()
}() }()
} }
@ -401,17 +431,6 @@ func TestConcurrentHandleContext(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest(http.MethodGet, url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}
func TestTreeRunDynamicRouting(t *testing.T) { func TestTreeRunDynamicRouting(t *testing.T) {
router := New() router := New()
router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
@ -561,3 +580,28 @@ func TestTreeRunDynamicRouting(t *testing.T) {
func isWindows() bool { func isWindows() bool {
return runtime.GOOS == "windows" return runtime.GOOS == "windows"
} }
func TestEscapedColon(t *testing.T) {
router := New()
f := func(u string) {
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
}
f("/r/r\\:r")
f("/r/r:r")
f("/r/r/:r")
f("/r/r/\\:r")
f("/r/r/r\\:r")
assert.Panics(t, func() {
f("\\foo:")
})
router.updateRouteTrees()
ts := httptest.NewServer(router)
defer ts.Close()
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
}

View File

@ -20,6 +20,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -45,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
}) })
router.GET("/raw", func(c *Context) { router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{ c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt
}) })
}) })
}) })
@ -72,7 +73,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -130,7 +131,7 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -150,7 +151,7 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -177,7 +178,7 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := client.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -197,7 +198,7 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) res, err := http.Get(ts.URL + "/raw")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -228,7 +229,7 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -248,7 +249,7 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -268,7 +269,7 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := http.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -295,7 +296,7 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL)) res, err := client.Get(ts.URL + "/test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -315,7 +316,116 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
) )
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL)) res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
var tmplFS = http.Dir("testdata/template")
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/raw")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -326,31 +436,31 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1) assert.Len(t, router.trees, 1)
assert.NotNil(t, router.trees.get("GET")) assert.NotNil(t, router.trees.get(http.MethodGet))
assert.Nil(t, router.trees.get("POST")) assert.Nil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
assert.NotNil(t, router.trees.get("GET")) assert.NotNil(t, router.trees.get(http.MethodGet))
assert.NotNil(t, router.trees.get("POST")) assert.NotNil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2) assert.Len(t, router.trees, 2)
} }
func TestAddRouteFails(t *testing.T) { func TestAddRouteFails(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) }) assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) }) assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) })
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Panics(t, func() { assert.Panics(t, func() {
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
}) })
} }
@ -435,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
} }
func TestRebuild404Handlers(t *testing.T) { func TestRebuild404Handlers(t *testing.T) {
var middleware0 HandlerFunc = func(c *Context) {}
var middleware1 HandlerFunc = func(c *Context) {}
router := New()
// Initially, allNoRoute should be nil
assert.Nil(t, router.allNoRoute)
// Set NoRoute handlers
router.NoRoute(middleware0)
assert.Len(t, router.allNoRoute, 1)
assert.Len(t, router.noRoute, 1)
compareFunc(t, router.allNoRoute[0], middleware0)
// Add Use middleware should trigger rebuild404Handlers
router.Use(middleware1)
assert.Len(t, router.allNoRoute, 2)
assert.Len(t, router.Handlers, 1)
assert.Len(t, router.noRoute, 1)
// Global middleware should come first
compareFunc(t, router.allNoRoute[0], middleware1)
compareFunc(t, router.allNoRoute[1], middleware0)
} }
func TestNoMethodWithGlobalHandlers(t *testing.T) { func TestNoMethodWithGlobalHandlers(t *testing.T) {
@ -492,27 +625,27 @@ func TestListOfRoutes(t *testing.T) {
assert.Len(t, list, 7) assert.Len(t, list, 7)
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: "GET", Method: http.MethodGet,
Path: "/favicon.ico", Path: "/favicon.ico",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: "GET", Method: http.MethodGet,
Path: "/", Path: "/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: "GET", Method: http.MethodGet,
Path: "/users/", Path: "/users/",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: "GET", Method: http.MethodGet,
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest1$",
}) })
assertRoutePresent(t, list, RouteInfo{ assertRoutePresent(t, list, RouteInfo{
Method: "POST", Method: http.MethodPost,
Path: "/users/:id", Path: "/users/:id",
Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$", Handler: "^(.*/vendor/)?github.com/gin-gonic/gin.handlerTest2$",
}) })
@ -530,7 +663,7 @@ func TestEngineHandleContext(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/") w := PerformRequest(r, http.MethodGet, "/")
assert.Equal(t, 301, w.Code) assert.Equal(t, 301, w.Code)
}) })
} }
@ -547,10 +680,10 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
r.GET("/:count", func(c *Context) { r.GET("/:count", func(c *Context) {
countStr := c.Param("count") countStr := c.Param("count")
count, err := strconv.Atoi(countStr) count, err := strconv.Atoi(countStr)
assert.NoError(t, err) require.NoError(t, err)
n, err := c.Writer.Write([]byte(".")) n, err := c.Writer.Write([]byte("."))
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, n) assert.Equal(t, 1, n)
switch { switch {
@ -563,7 +696,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
}) })
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value w := PerformRequest(r, http.MethodGet, "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len()) assert.Equal(t, expectValue, w.Body.Len())
}) })
@ -572,6 +705,93 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter) assert.Equal(t, int64(expectValue), middlewareCounter)
} }
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is Escaped, the %25 is not interpreted as %
assert.Equal(t, "foo%252Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) {
r := New()
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
// Path is used, the %25 is interpreted as %
assert.Equal(t, "foo%2Fbar", c.Param("path"))
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) {
r := New()
r.UseEscapedPath = true
r.UseRawPath = true
r.UnescapePathValues = false
r.GET("/v1/:path", func(c *Context) {
assert.Equal(t, "foo%25bar", c.Param("path"))
c.Status(http.StatusOK)
})
assert.NotPanics(t, func() {
w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar")
assert.Equal(t, 200, w.Code)
})
}
func TestPrepareTrustedCIRDsWith(t *testing.T) { func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New() r := New()
@ -580,7 +800,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("0.0.0.0/0")}
err := r.SetTrustedProxies([]string{"0.0.0.0/0"}) err := r.SetTrustedProxies([]string{"0.0.0.0/0"})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -588,7 +808,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.33/33"}) err := r.SetTrustedProxies([]string{"192.168.1.33/33"})
assert.Error(t, err) require.Error(t, err)
} }
// valid ipv4 address // valid ipv4 address
@ -597,7 +817,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies([]string{"192.168.1.33"}) err := r.SetTrustedProxies([]string{"192.168.1.33"})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -605,7 +825,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"192.168.1.256"}) err := r.SetTrustedProxies([]string{"192.168.1.256"})
assert.Error(t, err) require.Error(t, err)
} }
// valid ipv6 address // valid ipv6 address
@ -613,7 +833,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("2002:0000:0000:1234:abcd:ffff:c0a8:0101/128")}
err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"}) err := r.SetTrustedProxies([]string{"2002:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -621,7 +841,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101"})
assert.Error(t, err) require.Error(t, err)
} }
// valid ipv6 cidr // valid ipv6 cidr
@ -629,7 +849,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")} expectedTrustedCIDRs := []*net.IPNet{parseCIDR("::/0")}
err := r.SetTrustedProxies([]string{"::/0"}) err := r.SetTrustedProxies([]string{"::/0"})
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -637,7 +857,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
{ {
err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"}) err := r.SetTrustedProxies([]string{"gggg:0000:0000:1234:abcd:ffff:c0a8:0101/129"})
assert.Error(t, err) require.Error(t, err)
} }
// valid combination // valid combination
@ -653,7 +873,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.1", "172.16.0.1",
}) })
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs) assert.Equal(t, expectedTrustedCIDRs, r.trustedCIDRs)
} }
@ -665,7 +885,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
"172.16.0.256", "172.16.0.256",
}) })
assert.Error(t, err) require.Error(t, err)
} }
// nil value // nil value
@ -673,7 +893,7 @@ func TestPrepareTrustedCIRDsWith(t *testing.T) {
err := r.SetTrustedProxies(nil) err := r.SetTrustedProxies(nil)
assert.Nil(t, r.trustedCIDRs) assert.Nil(t, r.trustedCIDRs)
assert.Nil(t, err) require.NoError(t, err)
} }
} }
@ -699,7 +919,7 @@ func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {} func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) { func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) { fc := func(e *Engine) {
e.GET("/test1", handlerTest1) e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2) e.GET("/test2", handlerTest2)
@ -711,8 +931,8 @@ func TestNewOptionFunc(t *testing.T) {
r := New(fc) r := New(fc)
routes := r.Routes() routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"}) assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"}) assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
} }
func TestWithOptionFunc(t *testing.T) { func TestWithOptionFunc(t *testing.T) {
@ -728,14 +948,14 @@ func TestWithOptionFunc(t *testing.T) {
}) })
routes := r.Routes() routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"}) assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"}) assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "github.com/gin-gonic/gin.handlerTest2"})
} }
type Birthday string type Birthday string
func (b *Birthday) UnmarshalParam(param string) error { func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1)) *b = Birthday(strings.ReplaceAll(param, "-", "/"))
return nil return nil
} }
@ -748,9 +968,119 @@ func TestCustomUnmarshalStruct(t *testing.T) {
_ = ctx.BindQuery(&request) _ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday) ctx.JSON(200, request.Birthday)
}) })
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil) req := httptest.NewRequest(http.MethodGet, "/test?birthday=2000-01-01", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
route.ServeHTTP(w, req) route.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `"2000/01/01"`, w.Body.String()) assert.Equal(t, `"2000/01/01"`, w.Body.String())
} }
// Test the fix for https://github.com/gin-gonic/gin/issues/4002
func TestMethodNotAllowedNoRoute(t *testing.T) {
g := New()
g.HandleMethodNotAllowed = true
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
func TestLiteralColonWithRun(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.updateRouteTrees()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHandler(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
handler := router.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHTTPServer(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.GET("/test/:param", func(c *Context) {
c.JSON(http.StatusOK, H{"param": c.Param("param")})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Contains(t, w2.Body.String(), "foo")
}
// Test that updateRouteTrees is called only once
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.String(http.StatusOK, "ok")
})
for range 5 {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ok", w.Body.String())
}
}

View File

@ -10,10 +10,12 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type route struct { type route struct {
@ -295,9 +297,9 @@ func TestShouldBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.ShouldBindUri(&person)) require.NoError(t, c.ShouldBindUri(&person))
assert.True(t, person.Name != "") assert.NotEmpty(t, person.Name)
assert.True(t, person.ID != "") assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK") c.String(http.StatusOK, "ShouldBindUri test OK")
}) })
@ -317,9 +319,9 @@ func TestBindUri(t *testing.T) {
} }
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) { router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person var person Person
assert.NoError(t, c.BindUri(&person)) require.NoError(t, c.BindUri(&person))
assert.True(t, person.Name != "") assert.NotEmpty(t, person.Name)
assert.True(t, person.ID != "") assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "BindUri test OK") c.String(http.StatusOK, "BindUri test OK")
}) })
@ -338,7 +340,7 @@ func TestBindUriError(t *testing.T) {
} }
router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) { router.Handle(http.MethodGet, "/new/rest/:num", func(c *Context) {
var m Member var m Member
assert.Error(t, c.BindUri(&m)) require.Error(t, c.BindUri(&m))
}) })
path1, _ := exampleFromPath("/new/rest/:num") path1, _ := exampleFromPath("/new/rest/:num")
@ -410,7 +412,7 @@ func exampleFromPath(path string) (string, Params) {
} }
if start >= 0 { if start >= 0 {
if c == '/' { if c == '/' {
value := fmt.Sprint(rand.Intn(100000)) value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:i], Key: path[start:i],
Value: value, Value: value,
@ -424,7 +426,7 @@ func exampleFromPath(path string) (string, Params) {
} }
} }
if start >= 0 { if start >= 0 {
value := fmt.Sprint(rand.Intn(100000)) value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{ params = append(params, Param{
Key: path[start:], Key: path[start:],
Value: value, Value: value,

46
go.mod
View File

@ -1,38 +1,42 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.20 go 1.24.0
require ( require (
github.com/bytedance/sonic v1.11.6 github.com/bytedance/sonic v1.14.2
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.20.0 github.com/go-playground/validator/v10 v10.28.0
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.19.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.2.2 github.com/modern-go/reflect2 v1.0.2
github.com/stretchr/testify v1.9.0 github.com/pelletier/go-toml/v2 v2.2.4
github.com/ugorji/go/codec v1.2.12 github.com/quic-go/quic-go v0.57.1
golang.org/x/net v0.25.0 github.com/stretchr/testify v1.11.1
google.golang.org/protobuf v1.34.1 github.com/ugorji/go/codec v1.3.1
gopkg.in/yaml.v3 v3.0.1 golang.org/x/net v0.47.0
google.golang.org/protobuf v1.36.10
) )
require ( require (
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

105
go.sum
View File

@ -1,84 +1,95 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 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-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.20
package bytesconv package bytesconv
import ( import (

View File

@ -1,26 +0,0 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !go1.20
package bytesconv
import (
"unsafe"
)
// StringToBytes converts string to byte slice without a memory allocation.
func StringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}
// BytesToString converts byte slice to string without a memory allocation.
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -1,23 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build sonic && avx && (linux || windows || darwin) && amd64
package json
import "github.com/bytedance/sonic"
var (
json = sonic.ConfigStd
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

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

View File

@ -31,31 +31,31 @@ func TestLogger(t *testing.T) {
router.HEAD("/example", func(c *Context) {}) router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {}) router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100") PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more // I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather // like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go. // than individual functions. I'm not sure where these should go.
buffer.Reset() buffer.Reset()
PerformRequest(router, "POST", "/example") PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST") assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "PUT", "/example") PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT") assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "DELETE", "/example") PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE") assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
@ -77,9 +77,9 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "GET", "/notfound") PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
@ -95,31 +95,31 @@ func TestLoggerWithConfig(t *testing.T) {
router.HEAD("/example", func(c *Context) {}) router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {}) router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100") PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more // I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather // like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go. // than individual functions. I'm not sure where these should go.
buffer.Reset() buffer.Reset()
PerformRequest(router, "POST", "/example") PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST") assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "PUT", "/example") PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT") assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "DELETE", "/example") PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE") assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
@ -141,9 +141,9 @@ func TestLoggerWithConfig(t *testing.T) {
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset() buffer.Reset()
PerformRequest(router, "GET", "/notfound") PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
} }
@ -169,19 +169,19 @@ func TestLoggerWithFormatter(t *testing.T) {
) )
})) }))
router.GET("/example", func(c *Context) {}) router.GET("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100") PerformRequest(router, http.MethodGet, "/example?a=100")
// output test // output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]") assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
} }
func TestLoggerWithConfigFormatting(t *testing.T) { func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams var gotParam LogFormatterParams
var gotKeys map[string]any var gotKeys map[any]any
buffer := new(strings.Builder) buffer := new(strings.Builder)
router := New() router := New()
@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
gotKeys = c.Keys gotKeys = c.Keys
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
}) })
PerformRequest(router, "GET", "/example?a=100") PerformRequest(router, http.MethodGet, "/example?a=100")
// output test // output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]") assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100") assert.Contains(t, buffer.String(), "a=100")
@ -225,7 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, 200, gotParam.StatusCode) assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency) assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP) assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method) assert.Equal(t, http.MethodGet, gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path) assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage) assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys) assert.Equal(t, gotKeys, gotParam.Keys)
@ -239,7 +239,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: "GET", Method: http.MethodGet,
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
@ -250,7 +250,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Second * 5, Latency: time.Second * 5,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: "GET", Method: http.MethodGet,
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -260,7 +260,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: "GET", Method: http.MethodGet,
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: true, isTerm: true,
@ -271,17 +271,17 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200, StatusCode: 200,
Latency: time.Millisecond * 9876543210, Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20", ClientIP: "20.20.20.20",
Method: "GET", Method: http.MethodGet,
Path: "/", Path: "/",
ErrorMessage: "", ErrorMessage: "",
isTerm: false, isTerm: false,
} }
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m0s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 5s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m|\x1b[97;41m 2743h29m0s \x1b[0m| 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
@ -292,10 +292,10 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor() return p.MethodColor()
} }
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue") assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue")
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan") assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan")
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow") assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow")
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red") assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green") assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white") assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
@ -317,6 +317,23 @@ func TestColorForStatus(t *testing.T) {
assert.Equal(t, red, colorForStatus(2), "other things should be red") assert.Equal(t, red, colorForStatus(2), "other things should be red")
} }
func TestColorForLatency(t *testing.T) {
colorForLantency := func(latency time.Duration) string {
p := LogFormatterParams{
Latency: latency,
}
return p.LatencyColor()
}
assert.Equal(t, white, colorForLantency(time.Duration(0)), "0 should be white")
assert.Equal(t, white, colorForLantency(time.Millisecond*20), "20ms should be white")
assert.Equal(t, green, colorForLantency(time.Millisecond*150), "150ms should be green")
assert.Equal(t, cyan, colorForLantency(time.Millisecond*250), "250ms should be cyan")
assert.Equal(t, yellow, colorForLantency(time.Millisecond*600), "600ms should be yellow")
assert.Equal(t, magenta, colorForLantency(time.Millisecond*1500), "1.5s should be magenta")
assert.Equal(t, red, colorForLantency(time.Second*3), "other things should be red")
}
func TestResetColor(t *testing.T) { func TestResetColor(t *testing.T) {
p := LogFormatterParams{} p := LogFormatterParams{}
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor()) assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
@ -329,13 +346,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.Equal(t, true, p.IsOutputColor()) assert.True(t, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor()) assert.True(t, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor()) assert.False(t, p.IsOutputColor())
// test with isTerm flag false. // test with isTerm flag false.
p = LogFormatterParams{ p = LogFormatterParams{
@ -343,13 +360,13 @@ func TestIsOutputColor(t *testing.T) {
} }
consoleColorMode = autoColor consoleColorMode = autoColor
assert.Equal(t, false, p.IsOutputColor()) assert.False(t, p.IsOutputColor())
ForceConsoleColor() ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor()) assert.True(t, p.IsOutputColor())
DisableConsoleColor() DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor()) assert.False(t, p.IsOutputColor())
// reset console color mode. // reset console color mode.
consoleColorMode = autoColor consoleColorMode = autoColor
@ -369,15 +386,15 @@ func TestErrorLogger(t *testing.T) {
c.String(http.StatusInternalServerError, "hola!") c.String(http.StatusInternalServerError, "hola!")
}) })
w := PerformRequest(router, "GET", "/error") w := PerformRequest(router, http.MethodGet, "/error")
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String()) assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = PerformRequest(router, "GET", "/abort") w = PerformRequest(router, http.MethodGet, "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String()) assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = PerformRequest(router, "GET", "/print") w = PerformRequest(router, http.MethodGet, "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
@ -389,11 +406,11 @@ func TestLoggerWithWriterSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {}) router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {}) router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged") PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, "GET", "/skipped") PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
@ -407,11 +424,11 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {}) router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {}) router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged") PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, "GET", "/skipped") PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
@ -427,11 +444,11 @@ func TestLoggerWithConfigSkipper(t *testing.T) {
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) }) router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) }) router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
PerformRequest(router, "GET", "/logged") PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset() buffer.Reset()
PerformRequest(router, "GET", "/skipped") PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }

View File

@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, http.StatusOK, w.Code)
@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
signature += " X " signature += " X "
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature += " XX " signature += " XX "
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
@ -175,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
@ -196,14 +196,14 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next() c.Next()
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusGone, w.Code) assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature) assert.Equal(t, "ACB", signature)
} }
// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as // TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// as well as Abort // as well as Abort
func TestMiddlewareFailHandlersChain(t *testing.T) { func TestMiddlewareFailHandlersChain(t *testing.T) {
// SETUP // SETUP
@ -219,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C" signature += "C"
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
@ -246,8 +246,8 @@ func TestMiddlewareWrite(t *testing.T) {
}) })
}) })
w := PerformRequest(router, "GET", "/") w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
} }

16
mode.go
View File

@ -8,6 +8,7 @@ import (
"flag" "flag"
"io" "io"
"os" "os"
"sync/atomic"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
@ -44,8 +45,8 @@ var DefaultWriter io.Writer = os.Stdout
var DefaultErrorWriter io.Writer = os.Stderr var DefaultErrorWriter io.Writer = os.Stderr
var ( var (
ginMode = debugCode ginMode int32 = debugCode
modeName = DebugMode modeName atomic.Value
) )
func init() { func init() {
@ -65,16 +66,15 @@ func SetMode(value string) {
switch value { switch value {
case DebugMode: case DebugMode:
ginMode = debugCode atomic.StoreInt32(&ginMode, debugCode)
case ReleaseMode: case ReleaseMode:
ginMode = releaseCode atomic.StoreInt32(&ginMode, releaseCode)
case TestMode: case TestMode:
ginMode = testCode atomic.StoreInt32(&ginMode, testCode)
default: default:
panic("gin mode unknown: " + value + " (available mode: debug release test)") panic("gin mode unknown: " + value + " (available mode: debug release test)")
} }
modeName.Store(value)
modeName = value
} }
// DisableBindValidation closes the default validator. // DisableBindValidation closes the default validator.
@ -96,5 +96,5 @@ func EnableJsonDecoderDisallowUnknownFields() {
// Mode returns current gin mode. // Mode returns current gin mode.
func Mode() string { func Mode() string {
return modeName return modeName.Load().(string)
} }

View File

@ -5,8 +5,8 @@
package gin package gin
import ( import (
"flag"
"os" "os"
"sync/atomic"
"testing" "testing"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -18,31 +18,24 @@ func init() {
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, testCode, ginMode) assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
os.Unsetenv(EnvGinMode) os.Unsetenv(EnvGinMode)
SetMode("") SetMode("")
assert.Equal(t, testCode, ginMode) assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
tmp := flag.CommandLine
flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
SetMode("")
assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode())
flag.CommandLine = tmp
SetMode(DebugMode) SetMode(DebugMode)
assert.Equal(t, debugCode, ginMode) assert.Equal(t, int32(debugCode), atomic.LoadInt32(&ginMode))
assert.Equal(t, DebugMode, Mode()) assert.Equal(t, DebugMode, Mode())
SetMode(ReleaseMode) SetMode(ReleaseMode)
assert.Equal(t, releaseCode, ginMode) assert.Equal(t, int32(releaseCode), atomic.LoadInt32(&ginMode))
assert.Equal(t, ReleaseMode, Mode()) assert.Equal(t, ReleaseMode, Mode())
SetMode(TestMode) SetMode(TestMode)
assert.Equal(t, testCode, ginMode) assert.Equal(t, int32(testCode), atomic.LoadInt32(&ginMode))
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })

55
path.go
View File

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

View File

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

View File

@ -5,7 +5,9 @@
package gin package gin
import ( import (
"bufio"
"bytes" "bytes"
"cmp"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -17,13 +19,13 @@ import (
"runtime" "runtime"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
var ( const (
dunno = []byte("???") dunno = "???"
centerDot = []byte("·") stackSkip = 3
dot = []byte(".")
slash = []byte("/")
) )
// RecoveryFunc defines the function passable to CustomRecovery. // RecoveryFunc defines the function passable to CustomRecovery.
@ -69,25 +71,18 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
} }
} }
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
brokenPipe = true
}
if logger != nil { if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, headersToStr, reset) logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), headersToStr, err, stack, reset) timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
} else { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset) timeFormat(time.Now()), err, stack(stackSkip), reset)
} }
} }
if brokenPipe { if brokenPipe {
@ -103,6 +98,21 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
} }
} }
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
//
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
func secureRequestDump(r *http.Request) string {
httpRequest, _ := httputil.DumpRequest(r, false)
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
for i, line := range lines {
if strings.HasPrefix(line, "Authorization:") {
lines[i] = "Authorization: *"
}
}
return strings.Join(lines, "\r\n")
}
func defaultHandleRecovery(c *Context, _ any) { func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
} }
@ -112,8 +122,11 @@ func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently // As we loop, we open files and read them. These variables record the currently
// loaded file. // loaded file.
var lines [][]byte var (
var lastFile string nLine string
lastFile string
err error
)
for i := skip; ; i++ { // Skip the expected number of frames for i := skip; ; i++ { // Skip the expected number of frames
pc, file, line, ok := runtime.Caller(i) pc, file, line, ok := runtime.Caller(i)
if !ok { if !ok {
@ -122,34 +135,53 @@ func stack(skip int) []byte {
// Print this much at least. If we can't find the source, it won't show. // Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile { if file != lastFile {
data, err := os.ReadFile(file) nLine, err = readNthLine(file, line-1)
if err != nil { if err != nil {
continue continue
} }
lines = bytes.Split(data, []byte{'\n'})
lastFile = file lastFile = file
} }
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
} }
return buf.Bytes() return buf.Bytes()
} }
// source returns a space-trimmed slice of the n'th line. // readNthLine reads the nth line from the file.
func source(lines [][]byte, n int) []byte { // It returns the trimmed content of the line if found,
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed // or an empty string if the line doesn't exist.
if n < 0 || n >= len(lines) { // If there's an error opening the file, it returns the error.
return dunno func readNthLine(file string, n int) (string, error) {
if n < 0 {
return "", nil
} }
return bytes.TrimSpace(lines[n])
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for i := 0; i < n; i++ {
if !scanner.Scan() {
return "", nil
}
}
if scanner.Scan() {
return strings.TrimSpace(scanner.Text()), nil
}
return "", nil
} }
// function returns, if possible, the name of the function containing the PC. // function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte { func function(pc uintptr) string {
fn := runtime.FuncForPC(pc) fn := runtime.FuncForPC(pc)
if fn == nil { if fn == nil {
return dunno return dunno
} }
name := []byte(fn.Name()) name := fn.Name()
// The name includes the path name to the package, which is unnecessary // The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots. // since the file name is already included. Plus, it has center dots.
// That is, we see // That is, we see
@ -158,13 +190,13 @@ func function(pc uintptr) []byte {
// *T.ptrmethod // *T.ptrmethod
// Also the package path might contain dot (e.g. code.google.com/...), // Also the package path might contain dot (e.g. code.google.com/...),
// so first eliminate the path prefix // so first eliminate the path prefix
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
name = name[lastSlash+1:] name = name[lastSlash+1:]
} }
if period := bytes.Index(name, dot); period >= 0 { if period := strings.IndexByte(name, '.'); period >= 0 {
name = name[period+1:] name = name[period+1:]
} }
name = bytes.ReplaceAll(name, centerDot, dot) name = strings.ReplaceAll(name, "·", ".")
return name return name
} }

View File

@ -5,7 +5,6 @@
package gin package gin
import ( import (
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -26,14 +25,14 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery", w := PerformRequest(router, http.MethodGet, "/recovery",
header{ header{
Key: "Host", Key: "Host",
Value: "www.google.com", Value: "www.google.com",
}, },
header{ header{
Key: "Authorization", Key: "Authorization",
Value: fmt.Sprintf("Bearer %s", password), Value: "Bearer " + password,
}, },
header{ header{
Key: "Content-Type", Key: "Content-Type",
@ -56,7 +55,7 @@ func TestPanicInHandler(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -67,7 +66,7 @@ func TestPanicInHandler(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, "GET", "/recovery") w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -84,26 +83,11 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
} }
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, dunno, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, dunno, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
}
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
bs := function(1) bs := function(1)
assert.Equal(t, dunno, bs) assert.Equal(t, dunno, bs)
@ -135,7 +119,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e) panic(e)
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, expectCode, w.Code) assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg) assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
@ -143,6 +127,30 @@ func TestPanicWithBrokenPipe(t *testing.T) {
} }
} }
// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe
func TestPanicWithAbortHandler(t *testing.T) {
const expectCode = 204
var buf strings.Builder
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Panic with ErrAbortHandler which should be treated as broken pipe
panic(http.ErrAbortHandler)
})
// RUN
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
out := buf.String()
assert.Contains(t, out, "net/http: abort Handler")
assert.NotContains(t, out, "panic recovered")
}
func TestCustomRecoveryWithWriter(t *testing.T) { func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder) errBuffer := new(strings.Builder)
buffer := new(strings.Builder) buffer := new(strings.Builder)
@ -156,7 +164,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -167,7 +175,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, "GET", "/recovery") w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -191,7 +199,7 @@ func TestCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -202,7 +210,7 @@ func TestCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, "GET", "/recovery") w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -226,7 +234,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := PerformRequest(router, "GET", "/recovery") w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
@ -237,7 +245,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
// Debug mode prints the request // Debug mode prints the request
SetMode(DebugMode) SetMode(DebugMode)
// RUN // RUN
w = PerformRequest(router, "GET", "/recovery") w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST // TEST
assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
@ -246,3 +254,115 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
SetMode(TestMode) SetMode(TestMode)
} }
func TestSecureRequestDump(t *testing.T) {
tests := []struct {
name string
req *http.Request
wantContains string
wantNotContain string
}{
{
name: "Authorization header standard case",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("Authorization", "Bearer secret-token")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "Bearer secret-token",
},
{
name: "authorization header lowercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("authorization", "some-secret")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "some-secret",
},
{
name: "Authorization header mixed case",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("AuThOrIzAtIoN", "token123")
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "token123",
},
{
name: "No Authorization header",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("Content-Type", "application/json")
return r
}(),
wantContains: "",
wantNotContain: "Authorization: *",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := secureRequestDump(tt.req)
if tt.wantContains != "" && !strings.Contains(result, tt.wantContains) {
t.Errorf("maskHeaders() = %q, want contains %q", result, tt.wantContains)
}
if tt.wantNotContain != "" && strings.Contains(result, tt.wantNotContain) {
t.Errorf("maskHeaders() = %q, want NOT contain %q", result, tt.wantNotContain)
}
})
}
}
// TestReadNthLine tests the readNthLine function with various scenarios.
func TestReadNthLine(t *testing.T) {
// Create a temporary test file
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
tempFile, err := os.CreateTemp("", "testfile*.txt")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tempFile.Name())
// Write test content to the temporary file
if _, err := tempFile.WriteString(testContent); err != nil {
t.Fatal(err)
}
if err := tempFile.Close(); err != nil {
t.Fatal(err)
}
// Test cases
tests := []struct {
name string
lineNum int
fileName string
want string
wantErr bool
}{
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := readNthLine(tt.fileName, tt.lineNum)
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, tt.want, got)
})
}
}
func BenchmarkStack(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
_ = stack(stackSkip)
}
}

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -29,7 +30,7 @@ func TestRenderMsgPack(t *testing.T) {
err := (MsgPack{data}).Render(w) err := (MsgPack{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
h := new(codec.MsgpackHandle) h := new(codec.MsgpackHandle)
assert.NotNil(t, h) assert.NotNil(t, h)
@ -37,7 +38,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NotNil(t, buf) assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data) err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, w.Body.String(), buf.String()) assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }

View File

@ -15,9 +15,10 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/codec/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@ -36,8 +37,8 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String()) assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -46,7 +47,7 @@ func TestRenderJSONError(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
assert.Error(t, (JSON{data}).Render(w)) require.Error(t, (JSON{data}).Render(w))
} }
func TestRenderIndentedJSON(t *testing.T) { func TestRenderIndentedJSON(t *testing.T) {
@ -58,8 +59,8 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String()) assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -69,7 +70,7 @@ func TestRenderIndentedJSONPanics(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
func TestRenderSecureJSON(t *testing.T) { func TestRenderSecureJSON(t *testing.T) {
@ -83,8 +84,8 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1) err1 := (SecureJSON{"while(1);", data}).Render(w1)
assert.NoError(t, err1) require.NoError(t, err1)
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String()) assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
@ -95,7 +96,7 @@ func TestRenderSecureJSON(t *testing.T) {
}} }}
err2 := (SecureJSON{"while(1);", datas}).Render(w2) err2 := (SecureJSON{"while(1);", datas}).Render(w2)
assert.NoError(t, err2) require.NoError(t, err2)
assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String()) assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
} }
@ -106,7 +107,7 @@ func TestRenderSecureJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (SecureJSON{"while(1);", data}).Render(w) err := (SecureJSON{"while(1);", data}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
func TestRenderJsonpJSON(t *testing.T) { func TestRenderJsonpJSON(t *testing.T) {
@ -120,7 +121,7 @@ func TestRenderJsonpJSON(t *testing.T) {
err1 := (JsonpJSON{"x", data}).Render(w1) err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1) require.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String()) assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
@ -132,7 +133,7 @@ func TestRenderJsonpJSON(t *testing.T) {
}} }}
err2 := (JsonpJSON{"x", datas}).Render(w2) err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2) require.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String()) assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
} }
@ -172,7 +173,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
err = jsonpJSON.Render(ew) err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`(`+`" error`, err.Error()) assert.Equal(t, `write "`+`(`+`" error`, err.Error())
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
ew.bufString = string(data) ew.bufString = string(data)
err = jsonpJSON.Render(ew) err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error()) assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
@ -191,9 +192,9 @@ func TestRenderJsonpJSONError2(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
e := (JsonpJSON{"", data}).Render(w) e := (JsonpJSON{"", data}).Render(w)
assert.NoError(t, e) require.NoError(t, e)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -203,7 +204,7 @@ func TestRenderJsonpJSONFail(t *testing.T) {
// json: unsupported type: chan int // json: unsupported type: chan int
err := (JsonpJSON{"x", data}).Render(w) err := (JsonpJSON{"x", data}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
func TestRenderAsciiJSON(t *testing.T) { func TestRenderAsciiJSON(t *testing.T) {
@ -215,15 +216,15 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1) err := (AsciiJSON{data1}).Render(w1)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String()) assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type")) assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder() w2 := httptest.NewRecorder()
data2 := 3.1415926 data2 := 3.1415926
err = (AsciiJSON{data2}).Render(w2) err = (AsciiJSON{data2}).Render(w2)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "3.1415926", w2.Body.String()) assert.Equal(t, "3.1415926", w2.Body.String())
} }
@ -232,7 +233,7 @@ func TestRenderAsciiJSONFail(t *testing.T) {
data := make(chan int) data := make(chan int)
// json: unsupported type: chan int // json: unsupported type: chan int
assert.Error(t, (AsciiJSON{data}).Render(w)) require.Error(t, (AsciiJSON{data}).Render(w))
} }
func TestRenderPureJSON(t *testing.T) { func TestRenderPureJSON(t *testing.T) {
@ -242,8 +243,8 @@ func TestRenderPureJSON(t *testing.T) {
"html": "<b>", "html": "<b>",
} }
err := (PureJSON{data}).Render(w) err := (PureJSON{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String()) assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -283,8 +284,15 @@ b:
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w) err := (YAML{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3
// We're checking that the output contains the expected data, not the exact formatting
output := w.Body.String()
assert.Contains(t, output, "a : Easy!")
assert.Contains(t, output, "b:")
assert.Contains(t, output, "c: 2")
assert.Contains(t, output, "d: [3, 4]")
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -298,7 +306,7 @@ func (ft *fail) MarshalYAML() (any, error) {
func TestRenderYAMLFail(t *testing.T) { func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w) err := (YAML{&fail{}}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
func TestRenderTOML(t *testing.T) { func TestRenderTOML(t *testing.T) {
@ -311,7 +319,7 @@ func TestRenderTOML(t *testing.T) {
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
err := (TOML{data}).Render(w) err := (TOML{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String()) assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -319,7 +327,7 @@ func TestRenderTOML(t *testing.T) {
func TestRenderTOMLFail(t *testing.T) { func TestRenderTOMLFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err := (TOML{net.IPv4bcast}).Render(w) err := (TOML{net.IPv4bcast}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
// test Protobuf rendering // test Protobuf rendering
@ -334,12 +342,12 @@ func TestRenderProtoBuf(t *testing.T) {
(ProtoBuf{data}).WriteContentType(w) (ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data) protoData, err := proto.Marshal(data)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
err = (ProtoBuf{data}).Render(w) err = (ProtoBuf{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, string(protoData), w.Body.String()) assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
} }
@ -348,7 +356,7 @@ func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := &testdata.Test{} data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w) err := (ProtoBuf{data}).Render(w)
assert.Error(t, err) require.Error(t, err)
} }
func TestRenderXML(t *testing.T) { func TestRenderXML(t *testing.T) {
@ -362,14 +370,14 @@ func TestRenderXML(t *testing.T) {
err := (XML{data}).Render(w) err := (XML{data}).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String()) assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderRedirect(t *testing.T) { func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest("GET", "/test-redirect", nil) req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
assert.NoError(t, err) require.NoError(t, err)
data1 := Redirect{ data1 := Redirect{
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
@ -379,7 +387,7 @@ func TestRenderRedirect(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
err = data1.Render(w) err = data1.Render(w)
assert.NoError(t, err) require.NoError(t, err)
data2 := Redirect{ data2 := Redirect{
Code: http.StatusOK, Code: http.StatusOK,
@ -390,7 +398,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() { assert.PanicsWithValue(t, "Cannot redirect with status code 200", func() {
err := data2.Render(w) err := data2.Render(w)
assert.NoError(t, err) require.NoError(t, err)
}) })
data3 := Redirect{ data3 := Redirect{
@ -401,7 +409,7 @@ func TestRenderRedirect(t *testing.T) {
w = httptest.NewRecorder() w = httptest.NewRecorder()
err = data3.Render(w) err = data3.Render(w)
assert.NoError(t, err) require.NoError(t, err)
// only improve coverage // only improve coverage
data2.WriteContentType(w) data2.WriteContentType(w)
@ -416,7 +424,7 @@ func TestRenderData(t *testing.T) {
Data: data, Data: data,
}).Render(w) }).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
} }
@ -435,7 +443,7 @@ func TestRenderString(t *testing.T) {
Data: []any{"manu", 2}, Data: []any{"manu", 2},
}).Render(w) }).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "hola manu 2", w.Body.String()) assert.Equal(t, "hola manu 2", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -448,7 +456,7 @@ func TestRenderStringLenZero(t *testing.T) {
Data: []any{}, Data: []any{},
}).Render(w) }).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "hola %s %d", w.Body.String()) assert.Equal(t, "hola %s %d", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -464,7 +472,7 @@ func TestRenderHTMLTemplate(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -480,7 +488,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String()) assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -488,10 +496,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) { func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"}, Files: []string{"../testdata/template/hello.tmpl"},
Glob: "", Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"}, FileSystem: nil,
FuncMap: nil, Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]any{ instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou", "name": "thinkerou",
@ -499,7 +509,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -507,10 +517,12 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) { func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{
Files: nil, Files: nil,
Glob: "../testdata/template/hello*", Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"}, FileSystem: nil,
FuncMap: nil, Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
} }
instance := htmlRender.Instance("hello.tmpl", map[string]any{ instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou", "name": "thinkerou",
@ -518,17 +530,40 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFS(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: http.Dir("../testdata/template"),
Patterns: []string{"hello.tmpl"},
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
err := instance.Render(w)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String()) assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderHTMLDebugPanics(t *testing.T) { func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{ htmlRender := HTMLDebug{
Files: nil, Files: nil,
Glob: "", Glob: "",
Delims: Delims{"{{", "}}"}, FileSystem: nil,
FuncMap: nil, Patterns: nil,
Delims: Delims{"{{", "}}"},
FuncMap: nil,
} }
assert.Panics(t, func() { htmlRender.Instance("", nil) }) assert.Panics(t, func() { htmlRender.Instance("", nil) })
} }
@ -548,7 +583,7 @@ func TestRenderReader(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
@ -571,7 +606,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
Headers: headers, Headers: headers,
}).Render(w) }).Render(w)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, body, w.Body.String()) assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type")) assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header()) assert.NotContains(t, "Content-Length", w.Header())
@ -580,7 +615,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
} }
func TestRenderWriteError(t *testing.T) { func TestRenderWriteError(t *testing.T) {
data := []interface{}{"value1", "value2"} data := []any{"value1", "value2"}
prefix := "my-prefix:" prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix} r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{ ew := &errorWriter{
@ -588,6 +623,6 @@ func TestRenderWriteError(t *testing.T) {
ResponseRecorder: httptest.NewRecorder(), ResponseRecorder: httptest.NewRecorder(),
} }
err := r.Render(ew) err := r.Render(ew)
assert.NotNil(t, err) require.Error(t, err)
assert.Equal(t, `write "my-prefix:" error`, err.Error()) assert.Equal(t, `write "my-prefix:" error`, err.Error())
} }

View File

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

View File

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

View File

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

View File

@ -5,11 +5,14 @@
package gin package gin
import ( import (
"bufio"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// TODO // TODO
@ -95,13 +98,13 @@ func TestResponseWriterWrite(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWriter.Code) assert.Equal(t, http.StatusOK, testWriter.Code)
assert.Equal(t, "hola", testWriter.Body.String()) assert.Equal(t, "hola", testWriter.Body.String())
assert.NoError(t, err) require.NoError(t, err)
n, err = w.Write([]byte(" adios")) n, err = w.Write([]byte(" adios"))
assert.Equal(t, 6, n) assert.Equal(t, 6, n)
assert.Equal(t, 10, w.Size()) assert.Equal(t, 10, w.Size())
assert.Equal(t, "hola adios", testWriter.Body.String()) assert.Equal(t, "hola adios", testWriter.Body.String())
assert.NoError(t, err) require.NoError(t, err)
} }
func TestResponseWriterHijack(t *testing.T) { func TestResponseWriterHijack(t *testing.T) {
@ -112,7 +115,7 @@ func TestResponseWriterHijack(t *testing.T) {
assert.Panics(t, func() { assert.Panics(t, func() {
_, _, err := w.Hijack() _, _, err := w.Hijack()
assert.NoError(t, err) require.NoError(t, err)
}) })
assert.True(t, w.Written()) assert.True(t, w.Written())
@ -123,6 +126,132 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush() w.Flush()
} }
type mockHijacker struct {
*httptest.ResponseRecorder
hijacked bool
}
// Hijack implements the http.Hijacker interface. It just records that it was called.
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
m.hijacked = true
return nil, nil, nil
}
func TestResponseWriterHijackAfterWrite(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error // Action to perform before hijacking
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack before write should succeed",
action: func(w ResponseWriter) error { return nil },
expectWrittenBeforeHijack: false,
expectHijackSuccess: true,
expectWrittenAfterHijack: true, // Hijack itself marks the writer as written
expectError: nil,
},
{
name: "hijack after write should fail",
action: func(w ResponseWriter) error {
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
// Check initial state
assert.False(t, w.Written(), "should not be written initially")
// Perform pre-hijack action
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
// Check state before hijacking
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
// Attempt to hijack
_, _, hijackErr := w.Hijack()
// Check results
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
action: func(w ResponseWriter) error {
w.WriteHeaderNow() // Simulate websocket.Accept() behavior
return nil
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: true, // NEW BEHAVIOR: allow hijack after just header write
expectWrittenAfterHijack: true,
expectError: nil,
},
{
name: "hijack after WriteHeaderNow + Write should fail",
action: func(w ResponseWriter) error {
w.WriteHeaderNow()
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
_, _, hijackErr := w.Hijack()
if tc.expectError == nil {
require.NoError(t, hijackErr, "expected hijack to succeed")
} else {
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
}
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
func TestResponseWriterFlush(t *testing.T) { func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{} writer := &responseWriter{}
@ -135,7 +264,7 @@ func TestResponseWriterFlush(t *testing.T) {
// should return 500 // should return 500
resp, err := http.Get(testServer.URL) resp, err := http.Get(testServer.URL)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
} }

View File

@ -218,7 +218,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) { return func(c *Context) {
if _, noListing := fs.(*onlyFilesFS); noListing { if _, noListing := fs.(*OnlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound) c.Writer.WriteHeader(http.StatusNotFound)
} }

View File

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

View File

@ -13,6 +13,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type header struct { type header struct {
@ -386,7 +387,7 @@ func TestRouteStaticFile(t *testing.T) {
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err) require.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -421,7 +422,7 @@ func TestRouteStaticFileFS(t *testing.T) {
} }
defer os.Remove(f.Name()) defer os.Remove(f.Name())
_, err = f.WriteString("Gin Web Framework") _, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err) require.NoError(t, err)
f.Close() f.Close()
dir, filename := filepath.Split(f.Name()) dir, filename := filepath.Split(f.Name())
@ -483,8 +484,8 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16, // Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8' // else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type")) assert.NotEmpty(t, w.Header().Get("Content-Type"))
assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified"))
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires")) assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN")) assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
} }
@ -522,8 +523,8 @@ func TestRouteNotAllowedEnabled3(t *testing.T) {
w := PerformRequest(router, http.MethodPut, "/path") w := PerformRequest(router, http.MethodPut, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
allowed := w.Header().Get("Allow") allowed := w.Header().Get("Allow")
assert.Contains(t, allowed, "GET") assert.Contains(t, allowed, http.MethodGet)
assert.Contains(t, allowed, "POST") assert.Contains(t, allowed, http.MethodPost)
} }
func TestRouteNotAllowedDisabled(t *testing.T) { func TestRouteNotAllowedDisabled(t *testing.T) {
@ -556,10 +557,10 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := PerformRequest(router, "GET", tr.route) w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location"))) assert.Equal(t, tr.location, w.Header().Get("Location"))
} }
} }
} }
@ -589,7 +590,7 @@ func TestRouterNotFound(t *testing.T) {
w := PerformRequest(router, http.MethodGet, tr.route) w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code) assert.Equal(t, tr.code, w.Code)
if w.Code != http.StatusNotFound { if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location"))) assert.Equal(t, tr.location, w.Header().Get("Location"))
} }
} }
@ -763,7 +764,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
// Test not found // Test not found
router.Use(func(c *Context) { router.Use(func(c *Context) {
// For not found routes full path is empty // For not found routes full path is empty
assert.Equal(t, "", c.FullPath()) assert.Empty(t, c.FullPath())
}) })
w := PerformRequest(router, http.MethodGet, "/not-found") w := PerformRequest(router, http.MethodGet, "/not-found")
@ -785,6 +786,6 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
v1.GET("/orgs/:id", handlerTest1) v1.GET("/orgs/:id", handlerTest1)
v1.DELETE("/orgs/:id", handlerTest1) v1.DELETE("/orgs/:id", handlerTest1)
w := PerformRequest(r, "GET", "/base/v1/user/groups") w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups")
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }

View File

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

2
testdata/test_file.txt vendored Normal file
View File

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

53
tree.go
View File

@ -5,7 +5,6 @@
package gin package gin
import ( import (
"bytes"
"net/url" "net/url"
"strings" "strings"
"unicode" "unicode"
@ -14,12 +13,6 @@ import (
"github.com/gin-gonic/gin/internal/bytesconv" "github.com/gin-gonic/gin/internal/bytesconv"
) )
var (
strColon = []byte(":")
strStar = []byte("*")
strSlash = []byte("/")
)
// Param is a single URL parameter, consisting of a key and a value. // Param is a single URL parameter, consisting of a key and a value.
type Param struct { type Param struct {
Key string Key string
@ -65,17 +58,10 @@ func (trees methodTrees) get(method string) *node {
return nil return nil
} }
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func longestCommonPrefix(a, b string) int { func longestCommonPrefix(a, b string) int {
i := 0 i := 0
max := min(len(a), len(b)) max_ := min(len(a), len(b))
for i < max && a[i] == b[i] { for i < max_ && a[i] == b[i] {
i++ i++
} }
return i return i
@ -92,16 +78,13 @@ func (n *node) addChild(child *node) {
} }
func countParams(path string) uint16 { func countParams(path string) uint16 {
var n uint16 colons := strings.Count(path, ":")
s := bytesconv.StringToBytes(path) stars := strings.Count(path, "*")
n += uint16(bytes.Count(s, strColon)) return safeUint16(colons + stars)
n += uint16(bytes.Count(s, strStar))
return n
} }
func countSections(path string) uint16 { func countSections(path string) uint16 {
s := bytesconv.StringToBytes(path) return safeUint16(strings.Count(path, "/"))
return uint16(bytes.Count(s, strSlash))
} }
type nodeType uint8 type nodeType uint8
@ -205,7 +188,7 @@ walk:
} }
// Check if a child with the next path byte exists // Check if a child with the next path byte exists
for i, max := 0, len(n.indices); i < max; i++ { for i, max_ := 0, len(n.indices); i < max_; i++ {
if c == n.indices[i] { if c == n.indices[i] {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
@ -241,7 +224,7 @@ walk:
// Wildcard conflict // Wildcard conflict
pathSeg := path pathSeg := path
if n.nType != catchAll { if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0] pathSeg, _, _ = strings.Cut(pathSeg, "/")
} }
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg + panic("'" + pathSeg +
@ -269,7 +252,19 @@ walk:
// Returns -1 as index, if no wildcard was found. // Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) { func findWildcard(path string) (wildcard string, i int, valid bool) {
// Find start // Find start
escapeColon := false
for start, c := range []byte(path) { for start, c := range []byte(path) {
if escapeColon {
escapeColon = false
if c == ':' {
continue
}
panic("invalid escape string in path '" + path + "'")
}
if c == '\\' {
escapeColon = true
continue
}
// A wildcard starts with ':' (param) or '*' (catch-all) // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' { if c != ':' && c != '*' {
continue continue
@ -353,7 +348,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := "" pathSeg := ""
if len(n.children) != 0 { if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0] pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
} }
panic("catch-all wildcard '" + path + panic("catch-all wildcard '" + path +
"' in new path '" + fullPath + "' in new path '" + fullPath +
@ -364,7 +359,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
// currently fixed width 1 for '/' // currently fixed width 1 for '/'
i-- i--
if path[i] != '/' { if i < 0 || path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'") panic("no / before catch-all in path '" + fullPath + "'")
} }
@ -378,7 +373,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
} }
n.addChild(child) n.addChild(child)
n.indices = string('/') n.indices = "/"
n = child n = child
n.priority++ n.priority++
@ -770,7 +765,7 @@ walk: // Outer loop for walking the tree
// Runes are up to 4 byte long, // Runes are up to 4 byte long,
// -4 would definitely be another rune. // -4 would definitely be another rune.
var off int var off int
for max := min(npLen, 3); off < max; off++ { for max_ := min(npLen, 3); off < max_; off++ {
if i := npLen - off; utf8.RuneStart(oldPath[i]) { if i := npLen - off; utf8.RuneStart(oldPath[i]) {
// read rune from cached path // read rune from cached path
rv, _ = utf8.DecodeRuneInString(oldPath[i:]) rv, _ = utf8.DecodeRuneInString(oldPath[i:])

View File

@ -192,6 +192,7 @@ func TestTreeWildcard(t *testing.T) {
"/get/abc/123abg/:param", "/get/abc/123abg/:param",
"/get/abc/123abf/:param", "/get/abc/123abf/:param",
"/get/abc/123abfff/:param", "/get/abc/123abfff/:param",
"/get/abc/escaped_colon/test\\:param",
} }
for _, route := range routes { for _, route := range routes {
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
@ -315,6 +316,7 @@ func TestTreeWildcard(t *testing.T) {
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}}, {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}}, {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}}, {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
}) })
checkPriorities(t, tree) checkPriorities(t, tree)
@ -419,6 +421,9 @@ func TestTreeWildcardConflict(t *testing.T) {
{"/id/:id", false}, {"/id/:id", false},
{"/static/*file", false}, {"/static/*file", false},
{"/static/", true}, {"/static/", true},
{"/escape/test\\:d1", false},
{"/escape/test\\:d2", false},
{"/escape/test:param", false},
} }
testRoutes(t, routes) testRoutes(t, routes)
} }
@ -476,7 +481,7 @@ func TestTreeDuplicatePath(t *testing.T) {
} }
} }
//printChildren(tree, "") // printChildren(tree, "")
checkRequests(t, tree, testRequests{ checkRequests(t, tree, testRequests{
{"/", false, "/", nil}, {"/", false, "/", nil},
@ -527,7 +532,7 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
func TestTreeCatchMaxParams(t *testing.T) { func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{} tree := &node{}
var route = "/cmd/*filepath" route := "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route)) tree.addRoute(route, fakeHandler(route))
} }
@ -687,7 +692,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
} }
func TestRedirectTrailingSlash(t *testing.T) { func TestRedirectTrailingSlash(t *testing.T) {
var data = []struct { data := []struct {
path string path string
}{ }{
{"/hello/:name"}, {"/hello/:name"},
@ -971,3 +976,45 @@ func TestTreeWildcardConflictEx(t *testing.T) {
} }
} }
} }
func TestTreeInvalidEscape(t *testing.T) {
routes := map[string]bool{
"/r1/r": true,
"/r2/:r": true,
"/r3/\\:r": true,
}
tree := &node{}
for route, valid := range routes {
recv := catchPanic(func() {
tree.addRoute(route, fakeHandler(route))
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
}
}
func TestWildcardInvalidSlash(t *testing.T) {
const panicMsgPrefix = "no / before catch-all in path"
routes := map[string]bool{
"/foo/bar": true,
"/foo/x*zy": false,
"/foo/b*r": false,
}
for route, valid := range routes {
tree := &node{}
recv := catchPanic(func() {
tree.addRoute(route, nil)
})
if recv == nil != valid {
t.Fatalf("%s should be %t but got %v", route, valid, recv)
}
if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) {
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv)
}
}
}

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"encoding/xml" "encoding/xml"
"math"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -162,3 +163,19 @@ func isASCII(s string) bool {
} }
return true return true
} }
// safeInt8 converts int to int8 safely, capping at math.MaxInt8
func safeInt8(n int) int8 {
if n > math.MaxInt8 {
return math.MaxInt8
}
return int8(n)
}
// safeUint16 converts int to uint16 safely, capping at math.MaxUint16
func safeUint16(n int) uint16 {
if n > math.MaxUint16 {
return math.MaxUint16
}
return uint16(n)
}

Some files were not shown because too many files have changed in this diff Show More