Merge tag 'tags/v1.11.0'

This commit is contained in:
2025-10-14 13:53:17 +03:00
72 changed files with 2578 additions and 930 deletions
+96 -2
View File
@@ -1,5 +1,99 @@
# 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
@@ -26,7 +120,7 @@
* 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)
@@ -488,7 +582,7 @@
- [FIX] Refactor render
- [FIX] Reworked tests
- [FIX] logger now supports cygwin
- [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] Use X-Forwarded-For before X-Real-IP
- [FIX] time.Time binding (#904)
## Gin 1.1.4
+6 -4
View File
@@ -19,7 +19,7 @@ If you need performance and good productivity, you will love Gin.
### Prerequisites
Gin requires [Go](https://go.dev/) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
Gin requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above.
### Getting Gin
@@ -62,7 +62,7 @@ func main() {
To run the code, use the `go run` command, like:
```sh
$ go run example.go
go run example.go
```
Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response!
@@ -83,7 +83,7 @@ See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gi
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
- [English](https://gin-gonic.com/docs/)
- [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/)
@@ -92,6 +92,8 @@ The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in
- [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/)
### Articles
@@ -141,7 +143,7 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
## Middleware
You can find many useful Gin middlewares at [gin-contrib](https://git.company.lan/gopkg/gin-contrib).
You can find many useful Gin middlewares at [gin-contrib](https://git.company.lan/gopkg/gin-contrib) and [gin-gonic/contrib](https://github.com/gin-gonic/contrib).
## Uses
+5 -5
View File
@@ -90,7 +90,7 @@ func TestBasicAuthSucceed(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
@@ -109,7 +109,7 @@ func TestBasicAuth401(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
@@ -129,7 +129,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/login", nil)
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
@@ -147,7 +147,7 @@ func TestBasicAuthForProxySucceed(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
@@ -166,7 +166,7 @@ func TestBasicAuthForProxy407(t *testing.T) {
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
router.ServeHTTP(w, req)
+12 -12
View File
@@ -14,21 +14,21 @@ import (
func BenchmarkOneRoute(B *testing.B) {
router := New()
router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkRecoveryMiddleware(B *testing.B) {
router := New()
router.Use(Recovery())
router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/")
runRequest(B, router, http.MethodGet, "/")
}
func BenchmarkLoggerMiddleware(B *testing.B) {
router := New()
router.Use(LoggerWithWriter(newMockWriter()))
router.GET("/", func(c *Context) {})
runRequest(B, router, "GET", "/")
runRequest(B, router, http.MethodGet, "/")
}
func BenchmarkManyHandlers(B *testing.B) {
@@ -37,7 +37,7 @@ func BenchmarkManyHandlers(B *testing.B) {
router.Use(func(c *Context) {})
router.Use(func(c *Context) {})
router.GET("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func Benchmark5Params(B *testing.B) {
@@ -45,7 +45,7 @@ func Benchmark5Params(B *testing.B) {
router := New()
router.Use(func(c *Context) {})
router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {})
runRequest(B, router, "GET", "/param/path/to/parameter/john/12345")
runRequest(B, router, http.MethodGet, "/param/path/to/parameter/john/12345")
}
func BenchmarkOneRouteJSON(B *testing.B) {
@@ -56,7 +56,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
router.GET("/json", func(c *Context) {
c.JSON(http.StatusOK, data)
})
runRequest(B, router, "GET", "/json")
runRequest(B, router, http.MethodGet, "/json")
}
func BenchmarkOneRouteHTML(B *testing.B) {
@@ -68,7 +68,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.GET("/html", func(c *Context) {
c.HTML(http.StatusOK, "index", "hola")
})
runRequest(B, router, "GET", "/html")
runRequest(B, router, http.MethodGet, "/html")
}
func BenchmarkOneRouteSet(B *testing.B) {
@@ -76,7 +76,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
router.GET("/ping", func(c *Context) {
c.Set("key", "value")
})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkOneRouteString(B *testing.B) {
@@ -84,13 +84,13 @@ func BenchmarkOneRouteString(B *testing.B) {
router.GET("/text", func(c *Context) {
c.String(http.StatusOK, "this is a plain text")
})
runRequest(B, router, "GET", "/text")
runRequest(B, router, http.MethodGet, "/text")
}
func BenchmarkManyRoutesFist(B *testing.B) {
router := New()
router.Any("/ping", func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func BenchmarkManyRoutesLast(B *testing.B) {
@@ -103,7 +103,7 @@ func Benchmark404(B *testing.B) {
router := New()
router.Any("/something", func(c *Context) {})
router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/ping")
runRequest(B, router, http.MethodGet, "/ping")
}
func Benchmark404Many(B *testing.B) {
@@ -118,7 +118,7 @@ func Benchmark404Many(B *testing.B) {
router.GET("/user/:id/:mode", func(c *Context) {})
router.NoRoute(func(c *Context) {})
runRequest(B, router, "GET", "/viewfake")
runRequest(B, router, http.MethodGet, "/viewfake")
}
type mockWriter struct {
+5 -4
View File
@@ -8,6 +8,7 @@ package binding
import (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -39,20 +40,20 @@ func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body,
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
require.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK))
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2))
}
+134 -130
View File
@@ -51,8 +51,6 @@ type FooBarFileStruct struct {
type FooBarFileFailStruct struct {
FooBarStruct
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
// for unexport test
data *multipart.FileHeader `form:"data" binding:"required"`
}
type FooDefaultBarStruct struct {
@@ -69,15 +67,19 @@ type FooStructDisallowUnknownFields struct {
}
type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmiCrO"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixMilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"unixMicro"`
}
type FooStructForTimeTypeNotFormat struct {
@@ -145,31 +147,31 @@ type FooStructForMapPtrType struct {
}
func TestBindingDefault(t *testing.T) {
assert.Equal(t, Form, Default("GET", ""))
assert.Equal(t, Form, Default("GET", MIMEJSON))
assert.Equal(t, Form, Default(http.MethodGet, ""))
assert.Equal(t, Form, Default(http.MethodGet, MIMEJSON))
assert.Equal(t, JSON, Default("POST", MIMEJSON))
assert.Equal(t, JSON, Default("PUT", MIMEJSON))
assert.Equal(t, JSON, Default(http.MethodPost, MIMEJSON))
assert.Equal(t, JSON, Default(http.MethodPut, MIMEJSON))
assert.Equal(t, XML, Default("POST", MIMEXML))
assert.Equal(t, XML, Default("PUT", MIMEXML2))
assert.Equal(t, XML, Default(http.MethodPost, MIMEXML))
assert.Equal(t, XML, Default(http.MethodPut, MIMEXML2))
assert.Equal(t, Form, Default("POST", MIMEPOSTForm))
assert.Equal(t, Form, Default("PUT", MIMEPOSTForm))
assert.Equal(t, Form, Default(http.MethodPost, MIMEPOSTForm))
assert.Equal(t, Form, Default(http.MethodPut, MIMEPOSTForm))
assert.Equal(t, FormMultipart, Default("POST", MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default("PUT", MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm))
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF))
assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
assert.Equal(t, YAML, Default("POST", MIMEYAML2))
assert.Equal(t, YAML, Default("PUT", MIMEYAML2))
assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML))
assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML))
assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML2))
assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML2))
assert.Equal(t, TOML, Default("POST", MIMETOML))
assert.Equal(t, TOML, Default("PUT", MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
}
func TestBindingJSONNilBody(t *testing.T) {
@@ -227,137 +229,137 @@ func TestBindingJSONStringMap(t *testing.T) {
}
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
testFormBinding(t, http.MethodPost,
"/", "/",
"foo=bar&bar=foo", "bar2=foo")
}
func TestBindingForm2(t *testing.T) {
testFormBinding(t, "GET",
testFormBinding(t, http.MethodGet,
"/?foo=bar&bar=foo", "/?bar2=foo",
"", "")
}
func TestBindingFormEmbeddedStruct(t *testing.T) {
testFormBindingEmbeddedStruct(t, "POST",
testFormBindingEmbeddedStruct(t, http.MethodPost,
"/", "/",
"page=1&size=2&appkey=test-appkey", "bar2=foo")
}
func TestBindingFormEmbeddedStruct2(t *testing.T) {
testFormBindingEmbeddedStruct(t, "GET",
testFormBindingEmbeddedStruct(t, http.MethodGet,
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
"", "")
}
func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST",
testFormBindingDefaultValue(t, http.MethodPost,
"/", "/",
"foo=bar", "bar2=foo")
}
func TestBindingFormDefaultValue2(t *testing.T) {
testFormBindingDefaultValue(t, "GET",
testFormBindingDefaultValue(t, http.MethodGet,
"/?foo=bar", "/?bar2=foo",
"", "")
}
func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST",
testFormBindingForTime(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST",
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
testFormBindingForTimeFailFormat(t, "POST",
testFormBindingForTimeFailFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
testFormBindingForTimeFailLocation(t, "POST",
testFormBindingForTimeFailLocation(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
}
func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
testFormBindingForTime(t, http.MethodGet,
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "/?bar2=foo",
"", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET",
"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, http.MethodGet,
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
testFormBindingForTimeFailFormat(t, "GET",
testFormBindingForTimeFailFormat(t, http.MethodGet,
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
testFormBindingForTimeFailLocation(t, "GET",
testFormBindingForTimeFailLocation(t, http.MethodGet,
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
}
func TestFormBindingIgnoreField(t *testing.T) {
testFormBindingIgnoreField(t, "POST",
testFormBindingIgnoreField(t, http.MethodPost,
"/", "/",
"-=bar", "")
}
func TestBindingFormInvalidName(t *testing.T) {
testFormBindingInvalidName(t, "POST",
testFormBindingInvalidName(t, http.MethodPost,
"/", "/",
"test_name=bar", "bar2=foo")
}
func TestBindingFormInvalidName2(t *testing.T) {
testFormBindingInvalidName2(t, "POST",
testFormBindingInvalidName2(t, http.MethodPost,
"/", "/",
"map_foo=bar", "bar2=foo")
}
func TestBindingFormForType(t *testing.T) {
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"map_foo={\"bar\":123}", "map_foo=1", "Map")
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"slice_foo=1&slice_foo=2", "bar2=1&bar2=2", "Slice")
testFormBindingForType(t, "GET",
testFormBindingForType(t, http.MethodGet,
"/?slice_foo=1&slice_foo=2", "/?bar2=1&bar2=2",
"", "", "Slice")
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"slice_map_foo=1&slice_map_foo=2", "bar2=1&bar2=2", "SliceMap")
testFormBindingForType(t, "GET",
testFormBindingForType(t, http.MethodGet,
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
"", "", "SliceMap")
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"ptr_bar=test", "bar2=test", "Ptr")
testFormBindingForType(t, "GET",
testFormBindingForType(t, http.MethodGet,
"/?ptr_bar=test", "/?bar2=test",
"", "", "Ptr")
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"idx=123", "id1=1", "Struct")
testFormBindingForType(t, "GET",
testFormBindingForType(t, http.MethodGet,
"/?idx=123", "/?id1=1",
"", "", "Struct")
testFormBindingForType(t, "POST",
testFormBindingForType(t, http.MethodPost,
"/", "/",
"name=thinkerou", "name1=ou", "StructPointer")
testFormBindingForType(t, "GET",
testFormBindingForType(t, http.MethodGet,
"/?name=thinkerou", "/?name1=ou",
"", "", "StructPointer")
}
@@ -374,7 +376,7 @@ func TestBindingFormStringMap(t *testing.T) {
func TestBindingFormStringSliceMap(t *testing.T) {
obj := make(map[string][]string)
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req := requestWithBody(http.MethodPost, "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err := Form.Bind(req, &obj)
require.NoError(t, err)
@@ -387,38 +389,38 @@ func TestBindingFormStringSliceMap(t *testing.T) {
assert.True(t, reflect.DeepEqual(obj, target))
objInvalid := make(map[string][]int)
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req = requestWithBody(http.MethodPost, "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err = Form.Bind(req, &objInvalid)
require.Error(t, err)
}
func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
testQueryBinding(t, http.MethodPost,
"/?foo=bar&bar=foo", "/",
"foo=unused", "bar2=foo")
}
func TestBindingQuery2(t *testing.T) {
testQueryBinding(t, "GET",
testQueryBinding(t, http.MethodGet,
"/?foo=bar&bar=foo", "/?bar2=foo",
"foo=unused", "")
}
func TestBindingQueryFail(t *testing.T) {
testQueryBindingFail(t, "POST",
testQueryBindingFail(t, http.MethodPost,
"/?map_foo=", "/",
"map_foo=unused", "bar2=foo")
}
func TestBindingQueryFail2(t *testing.T) {
testQueryBindingFail(t, "GET",
testQueryBindingFail(t, http.MethodGet,
"/?map_foo=", "/?bar2=foo",
"map_foo=unused", "")
}
func TestBindingQueryBoolFail(t *testing.T) {
testQueryBindingBoolFail(t, "GET",
testQueryBindingBoolFail(t, http.MethodGet,
"/?bool_foo=fasl", "/?bar2=foo",
"bool_foo=unused", "")
}
@@ -427,7 +429,7 @@ func TestBindingQueryStringMap(t *testing.T) {
b := Query
obj := make(map[string]string)
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
req := requestWithBody(http.MethodGet, "/?foo=bar&hello=world", "")
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.NotNil(t, obj)
@@ -436,7 +438,7 @@ func TestBindingQueryStringMap(t *testing.T) {
assert.Equal(t, "world", obj["hello"])
obj = make(map[string]string)
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
req = requestWithBody(http.MethodGet, "/?foo=bar&foo=2&hello=world", "") // should pick last
err = b.Bind(req, &obj)
require.NoError(t, err)
assert.NotNil(t, obj)
@@ -495,28 +497,28 @@ func TestBindingYAMLFail(t *testing.T) {
}
func createFormPostRequest(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createDefaultFormPostRequest(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormPostRequestForMap(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}"))
req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}"))
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormPostRequestForMapFail(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello"))
req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello"))
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
@@ -540,7 +542,7 @@ func createFormFilesMultipartRequest(t *testing.T) *http.Request {
_, err = io.Copy(fw, f)
require.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req, err2 := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body)
require.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -565,7 +567,7 @@ func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
_, err = io.Copy(fw, f)
require.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req, err2 := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body)
require.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -581,7 +583,7 @@ func createFormMultipartRequest(t *testing.T) *http.Request {
require.NoError(t, mw.SetBoundary(boundary))
require.NoError(t, mw.WriteField("foo", "bar"))
require.NoError(t, mw.WriteField("bar", "foo"))
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", body)
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
@@ -595,7 +597,7 @@ func createFormMultipartRequestForMap(t *testing.T) *http.Request {
require.NoError(t, mw.SetBoundary(boundary))
require.NoError(t, mw.WriteField("map_foo", "{\"bar\":123, \"name\":\"thinkerou\", \"pai\": 3.14}"))
req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", body)
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
@@ -609,7 +611,7 @@ func createFormMultipartRequestForMapFail(t *testing.T) *http.Request {
require.NoError(t, mw.SetBoundary(boundary))
require.NoError(t, mw.WriteField("map_foo", "3.14"))
req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
req, err := http.NewRequest(http.MethodPost, "/?map_foo=getfoo", body)
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
@@ -731,7 +733,7 @@ func TestBindingProtoBufFail(t *testing.T) {
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
err := JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -742,7 +744,7 @@ func TestValidationDisabled(t *testing.T) {
defer func() { Validator = backup }()
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
err := JSON.Bind(req, &obj)
require.NoError(t, err)
}
@@ -753,7 +755,7 @@ func TestRequiredSucceeds(t *testing.T) {
}
var obj HogeStruct
req := requestWithBody("POST", "/", `{"hoge": 0}`)
req := requestWithBody(http.MethodPost, "/", `{"hoge": 0}`)
err := JSON.Bind(req, &obj)
require.NoError(t, err)
}
@@ -764,7 +766,7 @@ func TestRequiredFails(t *testing.T) {
}
var obj HogeStruct
req := requestWithBody("POST", "/", `{"boen": 0}`)
req := requestWithBody(http.MethodPost, "/", `{"boen": 0}`)
err := JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -778,12 +780,12 @@ func TestHeaderBinding(t *testing.T) {
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req := requestWithBody(http.MethodGet, "/", "")
req.Header.Add("limit", "1000")
require.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req = requestWithBody(http.MethodGet, "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
@@ -843,7 +845,7 @@ func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, ba
obj := QueryTest{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -859,7 +861,7 @@ func testFormBinding(t *testing.T, method, path, badPath, body, badBody string)
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -879,7 +881,7 @@ func testFormBindingDefaultValue(t *testing.T, method, path, badPath, body, badB
obj := FooDefaultBarStruct{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -898,14 +900,14 @@ func TestFormBindingFail(t *testing.T) {
assert.Equal(t, "form", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
req, _ := http.NewRequest(http.MethodPost, "/", nil)
err := b.Bind(req, &obj)
require.Error(t, err)
}
func TestFormBindingMultipartFail(t *testing.T) {
obj := FooBarStruct{}
req, err := http.NewRequest("POST", "/", strings.NewReader("foo=bar"))
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader("foo=bar"))
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+";boundary=testboundary")
_, err = req.MultipartReader()
@@ -919,7 +921,7 @@ func TestFormPostBindingFail(t *testing.T) {
assert.Equal(t, "form-urlencoded", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
req, _ := http.NewRequest(http.MethodPost, "/", nil)
err := b.Bind(req, &obj)
require.Error(t, err)
}
@@ -929,7 +931,7 @@ func TestFormMultipartBindingFail(t *testing.T) {
assert.Equal(t, "multipart/form-data", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
req, _ := http.NewRequest(http.MethodPost, "/", nil)
err := b.Bind(req, &obj)
require.Error(t, err)
}
@@ -940,7 +942,7 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForTimeType{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -952,6 +954,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
assert.Equal(t, int64(1562400033001), obj.UnixMilliTime.UnixMilli())
assert.Equal(t, int64(1562400033000012), obj.UnixMicroTime.UnixMicro())
obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody)
@@ -965,7 +969,7 @@ func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, bo
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -983,7 +987,7 @@ func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body,
obj := FooStructForTimeTypeNotFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1001,7 +1005,7 @@ func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body,
obj := FooStructForTimeTypeFailFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1019,7 +1023,7 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod
obj := FooStructForTimeTypeFailLocation{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1037,7 +1041,7 @@ func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBo
obj := FooStructForIgnoreFormTag{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1052,12 +1056,12 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
obj := InvalidNameType{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "", obj.TestName)
assert.Empty(t, obj.TestName)
obj = InvalidNameType{}
req = requestWithBody(method, badPath, badBody)
@@ -1071,7 +1075,7 @@ func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badB
obj := InvalidNameMapType{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1088,7 +1092,7 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "form", b.Name())
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
switch typ {
@@ -1159,7 +1163,7 @@ func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string)
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1174,7 +1178,7 @@ func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody str
obj := FooStructForMapType{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1187,7 +1191,7 @@ func testQueryBindingBoolFail(t *testing.T, method, path, badPath, body, badBody
obj := FooStructForBoolType{}
req := requestWithBody(method, path, body)
if method == "POST" {
if method == http.MethodPost {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
@@ -1198,13 +1202,13 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -1213,19 +1217,19 @@ func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, ba
assert.Equal(t, name, b.Name())
var obj1 []FooStruct
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
err := b.Bind(req, &obj1)
require.NoError(t, err)
var obj2 []FooStruct
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj2)
require.Error(t, err)
}
func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string)
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
if b.Name() == "form" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
@@ -1238,13 +1242,13 @@ func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badB
if badPath != "" && badBody != "" {
obj = make(map[string]string)
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = b.Bind(req, &obj)
require.Error(t, err)
}
objInt := make(map[string]int)
req = requestWithBody("POST", path, body)
req = requestWithBody(http.MethodPost, path, body)
err = b.Bind(req, &objInt)
require.Error(t, err)
}
@@ -1253,7 +1257,7 @@ func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body
assert.Equal(t, name, b.Name())
obj := FooStructUseNumber{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
EnableDecoderUseNumber = true
err := b.Bind(req, &obj)
require.NoError(t, err)
@@ -1263,7 +1267,7 @@ func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body
assert.Equal(t, int64(123), v)
obj = FooStructUseNumber{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -1272,7 +1276,7 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.Equal(t, name, b.Name())
obj := FooStructUseNumber{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
EnableDecoderUseNumber = false
err := b.Bind(req, &obj)
require.NoError(t, err)
@@ -1281,7 +1285,7 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.InDelta(t, float64(123), obj.Foo, 0.01)
obj = FooStructUseNumber{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -1293,13 +1297,13 @@ func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath
}()
obj := FooStructDisallowUnknownFields{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStructDisallowUnknownFields{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj)
require.Error(t, err)
assert.Contains(t, err.Error(), "what")
@@ -1309,13 +1313,13 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
err := b.Bind(req, &obj)
require.Error(t, err)
assert.Equal(t, "", obj.Foo)
assert.Empty(t, obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
err = JSON.Bind(req, &obj)
require.Error(t, err)
}
@@ -1324,14 +1328,14 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba
assert.Equal(t, name, b.Name())
obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "yes", *obj.Label)
obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
require.Error(t, err)
@@ -1358,28 +1362,28 @@ func TestPlainBinding(t *testing.T) {
assert.Equal(t, "plain", p.Name())
var s string
req := requestWithBody("POST", "/", "test string")
req := requestWithBody(http.MethodPost, "/", "test string")
require.NoError(t, p.Bind(req, &s))
assert.Equal(t, "test string", s)
var bs []byte
req = requestWithBody("POST", "/", "test []byte")
req = requestWithBody(http.MethodPost, "/", "test []byte")
require.NoError(t, p.Bind(req, &bs))
assert.Equal(t, bs, []byte("test []byte"))
var i int
req = requestWithBody("POST", "/", "test fail")
req = requestWithBody(http.MethodPost, "/", "test fail")
require.Error(t, p.Bind(req, &i))
req = requestWithBody("POST", "/", "")
req = requestWithBody(http.MethodPost, "/", "")
req.Body = &failRead{}
require.Error(t, p.Bind(req, &s))
req = requestWithBody("POST", "/", "")
req = requestWithBody(http.MethodPost, "/", "")
require.NoError(t, p.Bind(req, nil))
var ptr *string
req = requestWithBody("POST", "/", "")
req = requestWithBody(http.MethodPost, "/", "")
require.NoError(t, p.Bind(req, ptr))
}
@@ -1387,7 +1391,7 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Equal(t, name, b.Name())
obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req := requestWithBody(http.MethodPost, path, body)
req.Body = io.NopCloser(&hook{})
req.Header.Add("Content-Type", MIMEPROTOBUF)
@@ -1402,7 +1406,7 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Equal(t, "obj is not ProtoMessage", err.Error())
obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
require.Error(t, err)
+4 -2
View File
@@ -18,14 +18,16 @@ func TestSliceValidationError(t *testing.T) {
{"has nil elements", SliceValidationError{errors.New("test error"), nil}, "[0]: test error"},
{"has zero elements", SliceValidationError{}, ""},
{"has one element", SliceValidationError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements",
{
"has two elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),
},
"[0]: first error\n[1]: second error",
},
{"has many elements",
{
"has many elements",
SliceValidationError{
errors.New("first error"),
errors.New("second error"),
+5 -3
View File
@@ -11,9 +11,11 @@ import (
const defaultMemory = 32 << 20
type formBinding struct{}
type formPostBinding struct{}
type formMultipartBinding struct{}
type (
formBinding struct{}
formPostBinding struct{}
formMultipartBinding struct{}
)
func (formBinding) Name() string {
return "form"
+15 -9
View File
@@ -13,8 +13,8 @@ import (
"strings"
"time"
"git.company.lan/gopkg/gin/codec/json"
"git.company.lan/gopkg/gin/internal/bytesconv"
"git.company.lan/gopkg/gin/internal/json"
)
var (
@@ -175,7 +175,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
// UnmarshalParam decodes and assigns a value from a form or query param.
UnmarshalParam(param string) error
}
@@ -333,9 +333,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case multipart.FileHeader:
return nil
}
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr:
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
@@ -398,18 +398,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
}
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
case "unix", "unixmilli", "unixmicro", "unixnano":
tv, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
var t time.Time
switch tf {
case "unix":
t = time.Unix(tv, 0)
case "unixmilli":
t = time.UnixMilli(tv)
case "unixmicro":
t = time.UnixMicro(tv)
default:
t = time.Unix(0, tv)
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
+22 -22
View File
@@ -6,7 +6,7 @@ package binding
import (
"encoding/hex"
"fmt"
"errors"
"mime/multipart"
"reflect"
"strconv"
@@ -494,7 +494,7 @@ type customUnmarshalParamType struct {
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid format")
return errors.New("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
@@ -509,9 +509,9 @@ func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
@@ -521,9 +521,9 @@ func TestMappingCustomStructTypeWithURITag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
@@ -533,9 +533,9 @@ func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
@@ -545,9 +545,9 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
assert.Equal(t, "file", s.FileData.Protocol)
assert.Equal(t, "/foo", s.FileData.Path)
assert.Equal(t, "happiness", s.FileData.Name)
}
type customPath []string
@@ -556,7 +556,7 @@ func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return fmt.Errorf("invalid format")
return errors.New("invalid format")
}
*p = elems
@@ -570,8 +570,8 @@ func TestMappingCustomSliceUri(t *testing.T) {
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
func TestMappingCustomSliceForm(t *testing.T) {
@@ -581,8 +581,8 @@ func TestMappingCustomSliceForm(t *testing.T) {
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
assert.Equal(t, "bar", s.FileData[0])
assert.Equal(t, "foo", s.FileData[1])
}
type objectID [12]byte
@@ -600,7 +600,7 @@ func (o *objectID) UnmarshalParam(param string) error {
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
if len(s) != 24 {
return nilObjectID, fmt.Errorf("invalid format")
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
@@ -621,7 +621,7 @@ func TestMappingCustomArrayUri(t *testing.T) {
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
assert.Equal(t, expected, s.FileData)
}
func TestMappingCustomArrayForm(t *testing.T) {
@@ -633,5 +633,5 @@ func TestMappingCustomArrayForm(t *testing.T) {
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
assert.Equal(t, expected, s.FileData)
}
-1
View File
@@ -17,7 +17,6 @@ func (headerBinding) Name() string {
}
func (headerBinding) Bind(req *http.Request, obj any) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"io"
"net/http"
"git.company.lan/gopkg/gin/internal/json"
"git.company.lan/gopkg/gin/codec/json"
)
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
}
func decodeJSON(r io.Reader, obj any) error {
decoder := json.NewDecoder(r)
decoder := json.API.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}
+186
View File
@@ -5,8 +5,16 @@
package binding
import (
"io"
"net/http/httptest"
"testing"
"time"
"unsafe"
"git.company.lan/gopkg/gin/codec/json"
"git.company.lan/gopkg/gin/render"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}
func TestCustomJsonCodec(t *testing.T) {
// Restore json encoding configuration after testing
oldMarshal := json.API
defer func() {
json.API = oldMarshal
}()
// Custom json api
json.API = customJsonApi{}
// test decode json
obj := customReq{}
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
require.NoError(t, err)
assert.Equal(t, zeroTime, obj.TimeEmpty)
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
assert.Nil(t, obj.TimeNil)
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
// test encode json
w := httptest.NewRecorder()
err2 := (render.PureJSON{Data: obj}).Render(w)
require.NoError(t, err2)
assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type customReq struct {
TimeEmpty time.Time `json:"time_empty"`
TimeStruct time.Time `json:"time_struct"`
TimeNil *time.Time `json:"time_nil"`
TimePointer *time.Time `json:"time_pointer"`
}
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
func init() {
customConfig.RegisterExtension(&TimeEx{})
customConfig.RegisterExtension(&TimePointerEx{})
}
type customJsonApi struct{}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
// region Time Extension
var (
zeroTime = time.Time{}
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
defaultTimeCodec = &timeCodec{}
)
type TimeEx struct {
jsoniter.DummyExtension
}
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
type timeCodec struct{}
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((*time.Time)(ptr))
return t.Equal(zeroTime)
}
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((*time.Time)(ptr))
if t.Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((*time.Time)(ptr)) = zeroTime
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((*time.Time)(ptr)) = t
}
// endregion
// region *Time Extension
var (
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
defaultTimePointerCodec = &timePointerCodec{}
)
type TimePointerEx struct {
jsoniter.DummyExtension
}
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
type timePointerCodec struct{}
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((**time.Time)(ptr))
return t == nil || (*t).Equal(zeroTime)
}
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((**time.Time)(ptr))
if t == nil || (*t).Equal(zeroTime) {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((**time.Time)(ptr)) = nil
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((**time.Time)(ptr)) = &t
}
// endregion
+1 -1
View File
@@ -116,7 +116,7 @@ func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request
err := mw.Close()
require.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
req, err := http.NewRequest(http.MethodPost, "/", &body)
require.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
+1 -1
View File
@@ -15,7 +15,7 @@ func (plainBinding) Name() string {
return "plain"
}
func (plainBinding) Bind(req *http.Request, obj interface{}) error {
func (plainBinding) Bind(req *http.Request, obj any) error {
all, err := io.ReadAll(req.Body)
if err != nil {
return err
+1 -1
View File
@@ -34,7 +34,7 @@ func (protobufBinding) BindBody(body []byte, obj any) error {
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but util now we can't add
// Here it's same to return validate(obj), but until now we can't add
// `binding:""` to the struct which automatically generate by gen-proto
return nil
// return validate(obj)
+1 -1
View File
@@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
return validate(obj)
}
+7 -7
View File
@@ -158,16 +158,16 @@ type structNoValidationPointer struct {
}
func TestValidateNoValidationPointers(t *testing.T) {
//origin := createNoValidation_values()
//test := createNoValidation_values()
// origin := createNoValidation_values()
// test := createNoValidation_values()
empty := structNoValidationPointer{}
//assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test))
// assert.Nil(t, validate(test))
// assert.Nil(t, validate(&test))
require.NoError(t, validate(empty))
require.NoError(t, validate(&empty))
//assert.Equal(t, origin, test)
// assert.Equal(t, origin, test)
}
type Object map[string]any
@@ -198,7 +198,7 @@ type structModifyValidation struct {
}
func toZero(sl validator.StructLevel) {
var s *structModifyValidation = sl.Top().Interface().(*structModifyValidation)
s := sl.Top().Interface().(*structModifyValidation)
s.Integer = 0
}
@@ -249,5 +249,5 @@ func TestValidatorEngine(t *testing.T) {
// Check that we got back non-nil errs
require.Error(t, errs)
// Check that the error matches expectation
require.Error(t, errs, "", "", "notone")
require.Error(t, errs, "notone")
}
+1
View File
@@ -24,6 +24,7 @@ func (xmlBinding) Bind(req *http.Request, obj any) error {
func (xmlBinding) BindBody(body []byte, obj any) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj any) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"io"
"net/http"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
)
type yamlBinding struct{}
+57
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
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
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
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
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)
}
+161 -187
View File
@@ -6,7 +6,9 @@ package gin
import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"math"
"mime/multipart"
@@ -71,7 +73,7 @@ type Context struct {
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]any
Keys map[any]any
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
@@ -128,7 +130,7 @@ func (c *Context) Copy() *Context {
cp.fullPath = c.fullPath
cKeys := c.Keys
cp.Keys = make(map[string]any, len(cKeys))
cp.Keys = make(map[any]any, len(cKeys))
c.mu.RLock()
for k, v := range cKeys {
cp.Keys[k] = v
@@ -186,10 +188,9 @@ func (c *Context) FullPath() string {
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] == nil {
continue
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
c.handlers[c.index](c)
c.index++
}
}
@@ -215,6 +216,14 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort()
}
// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
// This method stops the chain, writes the status code and return a JSON body without escaping.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
c.Abort()
c.PureJSON(code, jsonObj)
}
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json".
@@ -264,11 +273,11 @@ func (c *Context) Error(err error) *Error {
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
func (c *Context) Set(key any, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
c.Keys = make(map[any]any)
}
c.Keys[key] = value
@@ -276,7 +285,7 @@ func (c *Context) Set(key string, value any) {
// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
func (c *Context) Get(key any) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
@@ -284,255 +293,178 @@ func (c *Context) Get(key string) (value any, exists bool) {
}
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) any {
func (c *Context) MustGet(key any) any {
if value, exists := c.Get(key); exists {
return value
}
panic("Key \"" + key + "\" does not exist")
panic(fmt.Sprintf("key %v does not exist", key))
}
func getTyped[T any](c *Context, key any) (res T) {
if val, ok := c.Get(key); ok && val != nil {
res, _ = val.(T)
}
return
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
if val, ok := c.Get(key); ok && val != nil {
s, _ = val.(string)
}
return
func (c *Context) GetString(key any) (s string) {
return getTyped[string](c, key)
}
// GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) {
if val, ok := c.Get(key); ok && val != nil {
b, _ = val.(bool)
}
return
func (c *Context) GetBool(key any) (b bool) {
return getTyped[bool](c, key)
}
// GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) {
if val, ok := c.Get(key); ok && val != nil {
i, _ = val.(int)
}
return
func (c *Context) GetInt(key any) (i int) {
return getTyped[int](c, key)
}
// GetInt8 returns the value associated with the key as an integer 8.
func (c *Context) GetInt8(key string) (i8 int8) {
if val, ok := c.Get(key); ok && val != nil {
i8, _ = val.(int8)
}
return
func (c *Context) GetInt8(key any) (i8 int8) {
return getTyped[int8](c, key)
}
// GetInt16 returns the value associated with the key as an integer 16.
func (c *Context) GetInt16(key string) (i16 int16) {
if val, ok := c.Get(key); ok && val != nil {
i16, _ = val.(int16)
}
return
func (c *Context) GetInt16(key any) (i16 int16) {
return getTyped[int16](c, key)
}
// GetInt32 returns the value associated with the key as an integer 32.
func (c *Context) GetInt32(key string) (i32 int32) {
if val, ok := c.Get(key); ok && val != nil {
i32, _ = val.(int32)
}
return
func (c *Context) GetInt32(key any) (i32 int32) {
return getTyped[int32](c, key)
}
// GetInt64 returns the value associated with the key as an integer 64.
func (c *Context) GetInt64(key string) (i64 int64) {
if val, ok := c.Get(key); ok && val != nil {
i64, _ = val.(int64)
}
return
func (c *Context) GetInt64(key any) (i64 int64) {
return getTyped[int64](c, key)
}
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) {
if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
func (c *Context) GetUint(key any) (ui uint) {
return getTyped[uint](c, key)
}
// GetUint8 returns the value associated with the key as an unsigned integer 8.
func (c *Context) GetUint8(key string) (ui8 uint8) {
if val, ok := c.Get(key); ok && val != nil {
ui8, _ = val.(uint8)
}
return
func (c *Context) GetUint8(key any) (ui8 uint8) {
return getTyped[uint8](c, key)
}
// GetUint16 returns the value associated with the key as an unsigned integer 16.
func (c *Context) GetUint16(key string) (ui16 uint16) {
if val, ok := c.Get(key); ok && val != nil {
ui16, _ = val.(uint16)
}
return
func (c *Context) GetUint16(key any) (ui16 uint16) {
return getTyped[uint16](c, key)
}
// GetUint32 returns the value associated with the key as an unsigned integer 32.
func (c *Context) GetUint32(key string) (ui32 uint32) {
if val, ok := c.Get(key); ok && val != nil {
ui32, _ = val.(uint32)
}
return
func (c *Context) GetUint32(key any) (ui32 uint32) {
return getTyped[uint32](c, key)
}
// GetUint64 returns the value associated with the key as an unsigned integer 64.
func (c *Context) GetUint64(key string) (ui64 uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
}
return
func (c *Context) GetUint64(key any) (ui64 uint64) {
return getTyped[uint64](c, key)
}
// GetFloat32 returns the value associated with the key as a float32.
func (c *Context) GetFloat32(key string) (f32 float32) {
if val, ok := c.Get(key); ok && val != nil {
f32, _ = val.(float32)
}
return
func (c *Context) GetFloat32(key any) (f32 float32) {
return getTyped[float32](c, key)
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
f64, _ = val.(float64)
}
return
func (c *Context) GetFloat64(key any) (f64 float64) {
return getTyped[float64](c, key)
}
// GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) {
if val, ok := c.Get(key); ok && val != nil {
t, _ = val.(time.Time)
}
return
func (c *Context) GetTime(key any) (t time.Time) {
return getTyped[time.Time](c, key)
}
// GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) {
if val, ok := c.Get(key); ok && val != nil {
d, _ = val.(time.Duration)
}
return
func (c *Context) GetDuration(key any) (d time.Duration) {
return getTyped[time.Duration](c, key)
}
func (c *Context) GetIntSlice(key string) (is []int) {
if val, ok := c.Get(key); ok && val != nil {
is, _ = val.([]int)
}
return
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) (is []int) {
return getTyped[[]int](c, key)
}
func (c *Context) GetInt8Slice(key string) (i8s []int8) {
if val, ok := c.Get(key); ok && val != nil {
i8s, _ = val.([]int8)
}
return
// GetInt8Slice returns the value associated with the key as a slice of int8 integers.
func (c *Context) GetInt8Slice(key any) (i8s []int8) {
return getTyped[[]int8](c, key)
}
func (c *Context) GetInt16Slice(key string) (i16s []int16) {
if val, ok := c.Get(key); ok && val != nil {
i16s, _ = val.([]int16)
}
return
// GetInt16Slice returns the value associated with the key as a slice of int16 integers.
func (c *Context) GetInt16Slice(key any) (i16s []int16) {
return getTyped[[]int16](c, key)
}
func (c *Context) GetInt32Slice(key string) (i32s []int32) {
if val, ok := c.Get(key); ok && val != nil {
i32s, _ = val.([]int32)
}
return
// GetInt32Slice returns the value associated with the key as a slice of int32 integers.
func (c *Context) GetInt32Slice(key any) (i32s []int32) {
return getTyped[[]int32](c, key)
}
func (c *Context) GetInt64Slice(key string) (i64s []int64) {
if val, ok := c.Get(key); ok && val != nil {
i64s, _ = val.([]int64)
}
return
// GetInt64Slice returns the value associated with the key as a slice of int64 integers.
func (c *Context) GetInt64Slice(key any) (i64s []int64) {
return getTyped[[]int64](c, key)
}
func (c *Context) GetUintSlice(key string) (uis []uint) {
if val, ok := c.Get(key); ok && val != nil {
uis, _ = val.([]uint)
}
return
// GetUintSlice returns the value associated with the key as a slice of unsigned integers.
func (c *Context) GetUintSlice(key any) (uis []uint) {
return getTyped[[]uint](c, key)
}
func (c *Context) GetUint8Slice(key string) (ui8s []uint8) {
if val, ok := c.Get(key); ok && val != nil {
ui8s, _ = val.([]uint8)
}
return
// GetUint8Slice returns the value associated with the key as a slice of uint8 integers.
func (c *Context) GetUint8Slice(key any) (ui8s []uint8) {
return getTyped[[]uint8](c, key)
}
func (c *Context) GetUint16Slice(key string) (ui16s []uint16) {
if val, ok := c.Get(key); ok && val != nil {
ui16s, _ = val.([]uint16)
}
return
// GetUint16Slice returns the value associated with the key as a slice of uint16 integers.
func (c *Context) GetUint16Slice(key any) (ui16s []uint16) {
return getTyped[[]uint16](c, key)
}
func (c *Context) GetUint32Slice(key string) (ui32s []uint32) {
if val, ok := c.Get(key); ok && val != nil {
ui32s, _ = val.([]uint32)
}
return
// GetUint32Slice returns the value associated with the key as a slice of uint32 integers.
func (c *Context) GetUint32Slice(key any) (ui32s []uint32) {
return getTyped[[]uint32](c, key)
}
func (c *Context) GetUint64Slice(key string) (ui64s []uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64s, _ = val.([]uint64)
}
return
// GetUint64Slice returns the value associated with the key as a slice of uint64 integers.
func (c *Context) GetUint64Slice(key any) (ui64s []uint64) {
return getTyped[[]uint64](c, key)
}
func (c *Context) GetFloat32Slice(key string) (f32s []float32) {
if val, ok := c.Get(key); ok && val != nil {
f32s, _ = val.([]float32)
}
return
// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers.
func (c *Context) GetFloat32Slice(key any) (f32s []float32) {
return getTyped[[]float32](c, key)
}
func (c *Context) GetFloat64Slice(key string) (f64s []float64) {
if val, ok := c.Get(key); ok && val != nil {
f64s, _ = val.([]float64)
}
return
// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers.
func (c *Context) GetFloat64Slice(key any) (f64s []float64) {
return getTyped[[]float64](c, key)
}
// GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) {
if val, ok := c.Get(key); ok && val != nil {
ss, _ = val.([]string)
}
return
func (c *Context) GetStringSlice(key any) (ss []string) {
return getTyped[[]string](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]any) {
if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]any)
}
return
func (c *Context) GetStringMap(key any) (sm map[string]any) {
return getTyped[map[string]any](c, key)
}
// GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) {
if val, ok := c.Get(key); ok && val != nil {
sms, _ = val.(map[string]string)
}
return
func (c *Context) GetStringMapString(key any) (sms map[string]string) {
return getTyped[map[string]string](c, key)
}
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
if val, ok := c.Get(key); ok && val != nil {
smss, _ = val.(map[string][]string)
}
return
func (c *Context) GetStringMapStringSlice(key any) (smss map[string][]string) {
return getTyped[map[string][]string](c, key)
}
/************************************/
@@ -641,7 +573,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.initQueryCache()
return c.get(c.queryCache, key)
return getMapFromFormData(c.queryCache, key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
@@ -714,22 +646,32 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.initFormCache()
return c.get(c.formCache, key)
return getMapFromFormData(c.formCache, key)
}
// get is an internal method and returns a map which satisfies conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
// getMapFromFormData return a map which satisfies conditions.
// It parses from data with bracket notation like "key[subkey]=value" into a map.
func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
d := make(map[string]string)
found := false
keyLen := len(key)
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
}
if len(k) < keyLen+3 { // key + "[" + at least one char + "]"
continue
}
if k[:keyLen] != key || k[keyLen] != '[' {
continue
}
if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 {
found = true
d[k[keyLen+1:keyLen+1+j]] = v[0]
}
}
return dicts, exist
return d, found
}
// FormFile returns the first file for the provided form key.
@@ -754,14 +696,22 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
}
// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0o750); 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
}
@@ -838,8 +788,19 @@ func (c *Context) BindUri(obj any) error {
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
err := c.ShouldBindWith(obj, b)
if err != nil {
var maxBytesErr *http.MaxBytesError
// Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error
// https://github.com/goccy/go-json/issues/485
// https://github.com/bytedance/sonic/issues/800
switch {
case errors.As(err, &maxBytesErr):
c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck
default:
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
}
return err
}
return nil
@@ -958,7 +919,7 @@ func (c *Context) ShouldBindBodyWithPlain(obj any) error {
// ClientIP implements one best effort algorithm to return the real client IP.
// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not.
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]).
// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]).
// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy,
// the remote IP (coming from Request.RemoteAddr) is returned.
func (c *Context) ClientIP() string {
@@ -1096,6 +1057,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
})
}
// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers.
// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes.
// The provided cookie must have a valid Name. Invalid cookies may be silently dropped.
func (c *Context) SetCookieData(cookie *http.Cookie) {
if cookie.Path == "" {
cookie.Path = "/"
}
if cookie.SameSite == http.SameSiteDefaultMode {
cookie.SameSite = c.sameSite
}
http.SetCookie(c.Writer, cookie)
}
// Cookie returns the named cookie provided in the request or
// ErrNoCookie if not found. And return the named cookie is unescaped.
// If multiple cookies match the given name, only one cookie will
+35
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)
}
+710 -110
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -13,7 +13,7 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 21
const ginSupportMinGoVer = 23
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -25,7 +25,7 @@ func IsDebugging() bool {
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
// DebugPrintFunc indicates debug log output format.
var DebugPrintFunc func(format string, values ...interface{})
var DebugPrintFunc func(format string, values ...any)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() {
@@ -78,7 +78,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.21+.
debugPrint(`[WARNING] Now Gin requires Go 1.23+.
`)
}
+4 -3
View File
@@ -10,6 +10,7 @@ import (
"html/template"
"io"
"log"
"net/http"
"os"
"runtime"
"strings"
@@ -60,7 +61,7 @@ func TestDebugPrintError(t *testing.T) {
func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?git.company.lan/gopkg/gin.handlerNameTest \(2 handlers\)\n$`, re)
@@ -72,7 +73,7 @@ func TestDebugPrintRouteFunc(t *testing.T) {
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?git.company.lan/gopkg/gin.handlerNameTest \(2 handlers\)\n$`, re)
@@ -105,7 +106,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.21+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.23+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
+1 -1
View File
@@ -18,7 +18,7 @@ func TestBindWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`
+16
View File
@@ -2,5 +2,21 @@
Package gin implements a HTTP web framework called gin.
See https://gin-gonic.com/ for more information about gin.
Example:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
*/
package gin // import "git.company.lan/gopkg/gin"
+138 -16
View File
@@ -63,6 +63,7 @@
- [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie)
- [Custom json codec at runtime](#custom-json-codec-at-runtime)
- [Don't trust all proxies](#dont-trust-all-proxies)
- [Testing](#testing)
@@ -70,7 +71,7 @@
### Build with json replacement
Gin uses `encoding/json` as default json package but you can change it by build from other tags.
Gin uses `encoding/json` as the default JSON package but you can change it by building from other tags.
[jsoniter](https://github.com/json-iterator/go)
@@ -84,10 +85,10 @@ go build -tags=jsoniter .
go build -tags=go_json .
```
[sonic](https://github.com/bytedance/sonic) (you have to ensure that your cpu support avx instruction.)
[sonic](https://github.com/bytedance/sonic)
```sh
$ go build -tags="sonic avx" .
$ go build -tags=sonic .
```
### Build without `MsgPack` rendering feature
@@ -120,7 +121,7 @@ func main() {
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
// By default it serves on :8080 unless a
// By default, it serves on :8080 unless a
// PORT environment variable was defined.
router.Run()
// router.Run(":3000") for a hard coded port
@@ -172,7 +173,7 @@ func main() {
router := gin.Default()
// Query string parameters are parsed using the existing underlying request object.
// The request responds to an url matching: /welcome?firstname=Jane&lastname=Doe
// The request responds to a URL matching: /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
@@ -300,7 +301,7 @@ curl -X POST http://localhost:8080/upload \
#### Multiple files
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
```go
func main() {
@@ -704,7 +705,7 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
Skip validate: when running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
Skip-validation: Running the example above using the `curl` command returns an error. This is because the example uses `binding:"required"` for `Password`. If instead, you use `binding:"-"` for `Password`, then it will not return an error when you run the example again.
### Custom Validators
@@ -832,6 +833,8 @@ type Person struct {
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
}
func main() {
@@ -851,6 +854,8 @@ func startPage(c *gin.Context) {
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
log.Println(person.UnixMilliTime)
log.Println(person.UnixMicroTime)
}
c.String(http.StatusOK, "Success")
@@ -860,14 +865,14 @@ func startPage(c *gin.Context) {
Test it with:
```sh
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
```
### 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 (
@@ -1186,7 +1191,7 @@ func main() {
})
r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
// You can also use a struct
var msg struct {
Name string `json:"user"`
Message string
@@ -1392,13 +1397,19 @@ func main() {
### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles()
Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go
//go:embed templates/*
var templates embed.FS
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
//or
//router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
@@ -1485,7 +1496,7 @@ You may use custom delims
#### Custom Template Funcs
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
See the detailed [example code](https://github.com/gin-gonic/examples/tree/master/template).
main.go
@@ -1537,7 +1548,7 @@ Date: 2017/07/01
### Multitemplate
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
Gin allows only one html.Template by default. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
@@ -2086,7 +2097,7 @@ type formB struct {
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
// Calling c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
@@ -2293,12 +2304,64 @@ func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Using http.Cookie struct for more control
c.SetCookieData(&http.Cookie{
Name: "gin_cookie",
Value: "test",
Path: "/",
Domain: "localhost",
MaxAge: 3600,
Secure: false,
HttpOnly: true,
// Additional fields available in http.Cookie
Expires: time.Now().Add(24 * time.Hour),
// Partitioned: true, // Available in newer Go versions
})
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
You can also use the `SetCookieData` method, which accepts a `*http.Cookie` directly for more flexibility:
```go
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
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)
@@ -2308,6 +2371,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
Gin lets you specify which headers to hold the real client IP (if any),
@@ -2319,7 +2441,7 @@ or network CIDRs from where clients which their request headers related to clien
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
IPv6 CIDRs.
**Attention:** Gin trust all proxies by default if you don't specify a trusted
**Attention:** Gin trusts all proxies by default if you don't specify a trusted
proxy using the function above, **this is NOT safe**. At the same time, if you don't
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
then `Context.ClientIP()` will return the remote address directly to avoid some
+4 -4
View File
@@ -9,7 +9,7 @@ import (
"reflect"
"strings"
"git.company.lan/gopkg/gin/internal/json"
"git.company.lan/gopkg/gin/codec/json"
)
// 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.
func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON())
return json.API.Marshal(msg.JSON())
}
// 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()
func (msg *Error) Unwrap() error {
func (msg Error) Unwrap() error {
return msg.Err
}
@@ -157,7 +157,7 @@ func (a errorMsgs) JSON() any {
// MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON())
return json.API.Marshal(a.JSON())
}
func (a errorMsgs) String() string {
+18 -7
View File
@@ -9,7 +9,7 @@ import (
"fmt"
"testing"
"git.company.lan/gopkg/gin/internal/json"
"git.company.lan/gopkg/gin/codec/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -33,8 +33,8 @@ func TestError(t *testing.T) {
"meta": "some data",
}, err.JSON())
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
jsonBytes, _ := json.API.Marshal(err)
assert.JSONEq(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ //nolint: errcheck
"status": "200",
@@ -92,14 +92,14 @@ Error #03: third
H{"error": "second", "meta": "some data"},
H{"error": "third", "status": "400"},
}, errs.JSON())
jsonBytes, _ := json.Marshal(errs)
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
jsonBytes, _ := json.API.Marshal(errs)
assert.JSONEq(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
errs = errorMsgs{
{Err: errors.New("first"), Type: ErrorTypePrivate},
}
assert.Equal(t, H{"error": "first"}, errs.JSON())
jsonBytes, _ = json.Marshal(errs)
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
jsonBytes, _ = json.API.Marshal(errs)
assert.JSONEq(t, "{\"error\":\"first\"}", string(jsonBytes))
errs = errorMsgs{}
assert.Nil(t, errs.Last())
@@ -126,4 +126,15 @@ func TestErrorUnwrap(t *testing.T) {
require.ErrorIs(t, err, innerErr)
var testErr TestErr
require.ErrorAs(t, err, &testErr)
// Test non-pointer usage of gin.Error
errNonPointer := Error{
Err: innerErr,
Type: ErrorTypeAny,
}
wrappedErr := fmt.Errorf("wrapped: %w", errNonPointer)
// Check that 'errors.Is()' and 'errors.As()' behave as expected for non-pointer usage
require.ErrorIs(t, wrappedErr, innerErr)
var testErrNonPointer TestErr
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
}
-1
View File
@@ -17,7 +17,6 @@ type OnlyFilesFS struct {
// Open passes `Open` to the upstream implementation without `Readdir` functionality.
func (o OnlyFilesFS) Open(name string) (http.File, error) {
f, err := o.FileSystem.Open(name)
if err != nil {
return nil, err
}
+19 -3
View File
@@ -16,6 +16,7 @@ import (
"sync"
"git.company.lan/gopkg/gin/internal/bytesconv"
filesystem "git.company.lan/gopkg/gin/internal/fs"
"git.company.lan/gopkg/gin/render"
"github.com/quic-go/quic-go/http3"
@@ -215,7 +216,7 @@ func New(opts ...OptionFunc) *Engine {
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.engine = engine
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
@@ -285,6 +286,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ)
}
// LoadHTMLFS loads an http.FileSystem and a slice of patterns
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
filesystem.FileSystem{FileSystem: fs}, patterns...))
engine.SetHTMLTemplate(templ)
}
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
@@ -321,7 +335,7 @@ func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
return engine
}
// With returns a Engine with the configuration set in the OptionFunc.
// With returns an Engine with the configuration set in the OptionFunc.
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
@@ -363,7 +377,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
}
// Routes returns a slice of registered routes, including some useful information, such as:
// the http method, path and the handler name.
// the http method, path, and the handler name.
func (engine *Engine) Routes() (routes RoutesInfo) {
for _, tree := range engine.trees {
routes = iterate("", tree.method, routes, tree.root)
@@ -637,10 +651,12 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Disclaimer: You can loop yourself to deal with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
oldHandlers := c.handlers
c.reset()
engine.handleHTTPRequest(c)
c.index = oldIndexValue
c.handlers = oldHandlers
}
func (engine *Engine) handleHTTPRequest(c *Context) {
+10 -3
View File
@@ -12,8 +12,10 @@ import (
"git.company.lan/gopkg/gin"
)
var once sync.Once
var internalEngine *gin.Engine
var (
once sync.Once
internalEngine *gin.Engine
)
func engine() *gin.Engine {
once.Do(func() {
@@ -32,6 +34,11 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...)
}
// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
engine().LoadHTMLFS(fs, patterns...)
}
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
@@ -154,7 +161,7 @@ func RunUnix(file string) (err error) {
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
// Note: the method will block the calling goroutine indefinitely unless an error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}
+3 -4
View File
@@ -28,7 +28,6 @@ import (
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content) default:"it worked"
func testRequest(t *testing.T, params ...string) {
if len(params) == 0 {
t.Fatal("url cannot be empty")
}
@@ -47,12 +46,12 @@ func testRequest(t *testing.T, params ...string) {
body, ioerr := io.ReadAll(resp.Body)
require.NoError(t, ioerr)
var responseStatus = "200 OK"
responseStatus := "200 OK"
if len(params) > 1 && params[1] != "" {
responseStatus = params[1]
}
var responseBody = "it worked"
responseBody := "it worked"
if len(params) > 2 && params[2] != "" {
responseBody = params[2]
}
@@ -170,7 +169,7 @@ func TestRunTLS(t *testing.T) {
}
func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(`
html := template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
+184 -37
View File
@@ -46,7 +46,7 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]any{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), //nolint:gofumpt
})
})
})
@@ -73,7 +73,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -131,7 +131,7 @@ func TestLoadHTMLGlobTestMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -151,7 +151,7 @@ func TestLoadHTMLGlobReleaseMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -178,7 +178,7 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -198,7 +198,7 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
@@ -229,7 +229,7 @@ func TestLoadHTMLFilesTestMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -249,7 +249,7 @@ func TestLoadHTMLFilesDebugMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -269,7 +269,7 @@ func TestLoadHTMLFilesReleaseMode(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -296,7 +296,7 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
@@ -316,7 +316,116 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
var tmplFS = http.Dir("testdata/template")
func TestLoadHTMLFSTestMode(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSReleaseMode(t *testing.T) {
ts := setupHTMLFiles(
t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSUsingTLS(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get(ts.URL + "/test")
if err != nil {
t.Error(err)
}
resp, _ := io.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
}
func TestLoadHTMLFSFuncMap(t *testing.T) {
ts := setupHTMLFiles(
t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(ts.URL + "/raw")
if err != nil {
t.Error(err)
}
@@ -327,31 +436,31 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
func TestAddRoute(t *testing.T) {
router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1)
assert.NotNil(t, router.trees.get("GET"))
assert.Nil(t, router.trees.get("POST"))
assert.NotNil(t, router.trees.get(http.MethodGet))
assert.Nil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2)
assert.NotNil(t, router.trees.get("GET"))
assert.NotNil(t, router.trees.get("POST"))
assert.NotNil(t, router.trees.get(http.MethodGet))
assert.NotNil(t, router.trees.get(http.MethodPost))
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 2)
}
func TestAddRouteFails(t *testing.T) {
router := New()
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "a", HandlersChain{func(_ *Context) {}}) })
assert.Panics(t, func() { router.addRoute(http.MethodGet, "/", HandlersChain{}) })
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
assert.Panics(t, func() {
router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}})
router.addRoute(http.MethodPost, "/post", HandlersChain{func(_ *Context) {}})
})
}
@@ -493,27 +602,27 @@ func TestListOfRoutes(t *testing.T) {
assert.Len(t, list, 7)
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/favicon.ico",
Handler: "^(.*/vendor/)?git.company.lan/gopkg/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/",
Handler: "^(.*/vendor/)?git.company.lan/gopkg/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/users/",
Handler: "^(.*/vendor/)?git.company.lan/gopkg/gin.handlerTest2$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Method: http.MethodGet,
Path: "/users/:id",
Handler: "^(.*/vendor/)?git.company.lan/gopkg/gin.handlerTest1$",
})
assertRoutePresent(t, list, RouteInfo{
Method: "POST",
Method: http.MethodPost,
Path: "/users/:id",
Handler: "^(.*/vendor/)?git.company.lan/gopkg/gin.handlerTest2$",
})
@@ -531,7 +640,7 @@ func TestEngineHandleContext(t *testing.T) {
}
assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/")
w := PerformRequest(r, http.MethodGet, "/")
assert.Equal(t, 301, w.Code)
})
}
@@ -564,7 +673,7 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
})
assert.NotPanics(t, func() {
w := PerformRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
w := PerformRequest(r, http.MethodGet, "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
@@ -573,6 +682,44 @@ func TestEngineHandleContextManyReEntries(t *testing.T) {
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
// given
var handlerCounterV1, handlerCounterV2, middlewareCounterV1 int64
r := New()
v1 := r.Group("/v1")
{
v1.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounterV1, 1)
})
v1.GET("/test", func(c *Context) {
atomic.AddInt64(&handlerCounterV1, 1)
c.Status(http.StatusOK)
})
}
v2 := r.Group("/v2")
{
v2.GET("/test", func(c *Context) {
c.Request.URL.Path = "/v1/test"
r.HandleContext(c)
}, func(c *Context) {
atomic.AddInt64(&handlerCounterV2, 1)
})
}
// when
responseV1 := PerformRequest(r, "GET", "/v1/test")
responseV2 := PerformRequest(r, "GET", "/v2/test")
// then
assert.Equal(t, 200, responseV1.Code)
assert.Equal(t, 200, responseV2.Code)
assert.Equal(t, int64(2), handlerCounterV1)
assert.Equal(t, int64(2), middlewareCounterV1)
assert.Equal(t, int64(1), handlerCounterV2)
}
func TestPrepareTrustedCIRDsWith(t *testing.T) {
r := New()
@@ -700,7 +847,7 @@ func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {}
func TestNewOptionFunc(t *testing.T) {
var fc = func(e *Engine) {
fc := func(e *Engine) {
e.GET("/test1", handlerTest1)
e.GET("/test2", handlerTest2)
@@ -712,8 +859,8 @@ func TestNewOptionFunc(t *testing.T) {
r := New(fc)
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "git.company.lan/gopkg/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "git.company.lan/gopkg/gin.handlerTest2"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "git.company.lan/gopkg/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "git.company.lan/gopkg/gin.handlerTest2"})
}
func TestWithOptionFunc(t *testing.T) {
@@ -729,14 +876,14 @@ func TestWithOptionFunc(t *testing.T) {
})
routes := r.Routes()
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "git.company.lan/gopkg/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "git.company.lan/gopkg/gin.handlerTest2"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: http.MethodGet, Handler: "git.company.lan/gopkg/gin.handlerTest1"})
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: http.MethodGet, Handler: "git.company.lan/gopkg/gin.handlerTest2"})
}
type Birthday string
func (b *Birthday) UnmarshalParam(param string) error {
*b = Birthday(strings.Replace(param, "-", "/", -1))
*b = Birthday(strings.ReplaceAll(param, "-", "/"))
return nil
}
@@ -749,7 +896,7 @@ func TestCustomUnmarshalStruct(t *testing.T) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
})
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
req := httptest.NewRequest(http.MethodGet, "/test?birthday=2000-01-01", nil)
w := httptest.NewRecorder()
route.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
@@ -761,7 +908,7 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
g := New()
g.HandleMethodNotAllowed = true
req := httptest.NewRequest("GET", "/", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
+7 -6
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
@@ -297,8 +298,8 @@ func TestShouldBindUri(t *testing.T) {
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
require.NoError(t, c.ShouldBindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
assert.NotEmpty(t, person.Name)
assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
@@ -319,8 +320,8 @@ func TestBindUri(t *testing.T) {
router.Handle(http.MethodGet, "/rest/:name/:id", func(c *Context) {
var person Person
require.NoError(t, c.BindUri(&person))
assert.NotEqual(t, "", person.Name)
assert.NotEqual(t, "", person.ID)
assert.NotEmpty(t, person.Name)
assert.NotEmpty(t, person.ID)
c.String(http.StatusOK, "BindUri test OK")
})
@@ -411,7 +412,7 @@ func exampleFromPath(path string) (string, Params) {
}
if start >= 0 {
if c == '/' {
value := fmt.Sprint(rand.Intn(100000))
value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{
Key: path[start:i],
Value: value,
@@ -425,7 +426,7 @@ func exampleFromPath(path string) (string, Params) {
}
}
if start >= 0 {
value := fmt.Sprint(rand.Intn(100000))
value := strconv.Itoa(rand.Intn(100000))
params = append(params, Param{
Key: path[start:],
Value: value,
+21 -22
View File
@@ -1,45 +1,44 @@
module git.company.lan/gopkg/gin
go 1.23.6
go 1.23.11
require (
git.company.lan/gopkg/gin-sse v0.0.0-20250709115435-deecef94c1ae
github.com/bytedance/sonic v1.13.3
git.company.lan/gopkg/gin-sse v1.1.0
github.com/bytedance/sonic v1.14.0
github.com/go-playground/validator/v10 v10.27.0
github.com/goccy/go-json v0.10.5
github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.18.0
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.53.0
github.com/stretchr/testify v1.10.0
github.com/quic-go/quic-go v0.54.0
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.0
golang.org/x/net v0.41.0
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1
golang.org/x/net v0.42.0
google.golang.org/protobuf v1.36.9
)
require (
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+36 -50
View File
@@ -1,19 +1,16 @@
git.company.lan/gopkg/gin-sse v0.0.0-20250709115435-deecef94c1ae h1:Y2esYXNuB+rhi1wfaz3CBpIqQWySnXnbFrsFKb97E3A=
git.company.lan/gopkg/gin-sse v0.0.0-20250709115435-deecef94c1ae/go.mod h1:Bonf0ESOIe1LQBNFBkGNTYy9geegItybXhkOGmmydPI=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
git.company.lan/gopkg/gin-sse v1.1.0 h1:urYsv8wVYXjMbP6y17WudEsJcecQ5Y5ekymjEXZLE1A=
git.company.lan/gopkg/gin-sse v1.1.0/go.mod h1:Bonf0ESOIe1LQBNFBkGNTYy9geegItybXhkOGmmydPI=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/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=
@@ -22,80 +19,69 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+10 -4
View File
@@ -6,14 +6,17 @@ package bytesconv
import (
"bytes"
cRand "crypto/rand"
"math/rand"
"strings"
"testing"
"time"
)
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
var testBytes = []byte(testString)
var (
testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
testBytes = []byte(testString)
)
func rawBytesToStr(b []byte) string {
return string(b)
@@ -28,7 +31,10 @@ func rawStrToBytes(s string) []byte {
func TestBytesToString(t *testing.T) {
data := make([]byte, 1024)
for i := 0; i < 100; i++ {
rand.Read(data)
_, err := cRand.Read(data)
if err != nil {
t.Fatal(err)
}
if rawBytesToStr(data) != BytesToString(data) {
t.Fatal("don't match")
}
@@ -42,7 +48,7 @@ const (
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
var src = rand.New(rand.NewSource(time.Now().UnixNano()))
func RandStringBytesMaskImprSrcSB(n int) string {
sb := strings.Builder{}
+21
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
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)
}
-22
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
)
-22
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
)
-23
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
)
-23
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
)
+2 -2
View File
@@ -44,7 +44,7 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
// SkipPaths is an url path array which logs are not written.
// SkipPaths is a URL path array which logs are not written.
// Optional.
SkipPaths []string
@@ -82,7 +82,7 @@ type LogFormatterParams struct {
// BodySize is the size of the Response Body
BodySize int
// Keys are the keys set on the request's context.
Keys map[string]any
Keys map[any]any
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
+45 -45
View File
@@ -31,9 +31,9 @@ func TestLogger(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@@ -41,21 +41,21 @@ func TestLogger(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
PerformRequest(router, "POST", "/example")
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "PUT", "/example")
PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "DELETE", "/example")
PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
@@ -77,9 +77,9 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "GET", "/notfound")
PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound")
}
@@ -95,9 +95,9 @@ func TestLoggerWithConfig(t *testing.T) {
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@@ -105,21 +105,21 @@ func TestLoggerWithConfig(t *testing.T) {
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
PerformRequest(router, "POST", "/example")
PerformRequest(router, http.MethodPost, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), http.MethodPost)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "PUT", "/example")
PerformRequest(router, http.MethodPut, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), http.MethodPut)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "DELETE", "/example")
PerformRequest(router, http.MethodDelete, "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), http.MethodDelete)
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
@@ -141,9 +141,9 @@ func TestLoggerWithConfig(t *testing.T) {
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
PerformRequest(router, "GET", "/notfound")
PerformRequest(router, http.MethodGet, "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/notfound")
}
@@ -169,19 +169,19 @@ func TestLoggerWithFormatter(t *testing.T) {
)
}))
router.GET("/example", func(c *Context) {})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
}
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
var gotKeys map[string]any
var gotKeys map[any]any
buffer := new(strings.Builder)
router := New()
@@ -210,12 +210,12 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
gotKeys = c.Keys
time.Sleep(time.Millisecond)
})
PerformRequest(router, "GET", "/example?a=100")
PerformRequest(router, http.MethodGet, "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), http.MethodGet)
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
@@ -225,7 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, http.MethodGet, gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
@@ -239,7 +239,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: false,
@@ -250,7 +250,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: true,
@@ -260,7 +260,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: true,
@@ -271,7 +271,7 @@ func TestDefaultLogFormatter(t *testing.T) {
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Method: http.MethodGet,
Path: "/",
ErrorMessage: "",
isTerm: false,
@@ -292,10 +292,10 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor()
}
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, blue, colorForMethod(http.MethodGet), "get should be blue")
assert.Equal(t, cyan, colorForMethod(http.MethodPost), "post should be cyan")
assert.Equal(t, yellow, colorForMethod(http.MethodPut), "put should be yellow")
assert.Equal(t, red, colorForMethod(http.MethodDelete), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
@@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
c.String(http.StatusInternalServerError, "hola!")
})
w := PerformRequest(router, "GET", "/error")
w := PerformRequest(router, http.MethodGet, "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
assert.JSONEq(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = PerformRequest(router, "GET", "/abort")
w = PerformRequest(router, http.MethodGet, "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
assert.JSONEq(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = PerformRequest(router, "GET", "/print")
w = PerformRequest(router, http.MethodGet, "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
@@ -389,11 +389,11 @@ func TestLoggerWithWriterSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}
@@ -407,11 +407,11 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}
@@ -427,11 +427,11 @@ func TestLoggerWithConfigSkipper(t *testing.T) {
router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) })
router.GET("/skipped", func(c *Context) { c.Status(http.StatusNoContent) })
PerformRequest(router, "GET", "/logged")
PerformRequest(router, http.MethodGet, "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
PerformRequest(router, "GET", "/skipped")
PerformRequest(router, http.MethodGet, "/skipped")
assert.Contains(t, buffer.String(), "")
}
+10 -10
View File
@@ -35,7 +35,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
signature += " XX "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusOK, w.Code)
@@ -71,7 +71,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
signature += " X "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@@ -108,7 +108,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature += " XX "
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
@@ -149,7 +149,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
@@ -175,7 +175,7 @@ func TestMiddlewareAbort(t *testing.T) {
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusUnauthorized, w.Code)
@@ -196,14 +196,14 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
c.Next()
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature)
}
// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// as well as Abort
func TestMiddlewareFailHandlersChain(t *testing.T) {
// SETUP
@@ -219,7 +219,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
signature += "C"
})
// RUN
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
@@ -246,8 +246,8 @@ func TestMiddlewareWrite(t *testing.T) {
})
})
w := PerformRequest(router, "GET", "/")
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
}
+5 -3
View File
@@ -44,8 +44,10 @@ var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
var ginMode int32 = debugCode
var modeName atomic.Value
var (
ginMode int32 = debugCode
modeName atomic.Value
)
func init() {
mode := os.Getenv(EnvGinMode)
@@ -63,7 +65,7 @@ func SetMode(value string) {
}
switch value {
case DebugMode, "":
case DebugMode:
atomic.StoreInt32(&ginMode, debugCode)
case ReleaseMode:
atomic.StoreInt32(&ginMode, releaseCode)
+30 -25
View File
@@ -17,14 +17,13 @@ import (
"runtime"
"strings"
"time"
"git.company.lan/gopkg/gin/internal/bytesconv"
)
var (
dunno = []byte("???")
centerDot = []byte("·")
dot = []byte(".")
slash = []byte("/")
)
const dunno = "???"
var dunnoBytes = []byte(dunno)
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any)
@@ -70,24 +69,15 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
}
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")
const stackSkip = 3
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() {
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 {
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 {
@@ -103,6 +93,21 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
}
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
//
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
func secureRequestDump(r *http.Request) string {
httpRequest, _ := httputil.DumpRequest(r, false)
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
for i, line := range lines {
if strings.HasPrefix(line, "Authorization:") {
lines[i] = "Authorization: *"
}
}
return strings.Join(lines, "\r\n")
}
func defaultHandleRecovery(c *Context, _ any) {
c.AbortWithStatus(http.StatusInternalServerError)
}
@@ -138,18 +143,18 @@ func stack(skip int) []byte {
func source(lines [][]byte, n int) []byte {
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
if n < 0 || n >= len(lines) {
return dunno
return dunnoBytes
}
return bytes.TrimSpace(lines[n])
}
// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
func function(pc uintptr) string {
fn := runtime.FuncForPC(pc)
if fn == nil {
return dunno
}
name := []byte(fn.Name())
name := fn.Name()
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
@@ -158,13 +163,13 @@ func function(pc uintptr) []byte {
// *T.ptrmethod
// Also the package path might contain dot (e.g. code.google.com/...),
// so first eliminate the path prefix
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
name = name[lastSlash+1:]
}
if period := bytes.Index(name, dot); period >= 0 {
if period := strings.IndexByte(name, '.'); period >= 0 {
name = name[period+1:]
}
name = bytes.ReplaceAll(name, centerDot, dot)
name = strings.ReplaceAll(name, "·", ".")
return name
}
+76 -15
View File
@@ -5,7 +5,6 @@
package gin
import (
"fmt"
"net"
"net/http"
"os"
@@ -26,14 +25,14 @@ func TestPanicClean(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery",
w := PerformRequest(router, http.MethodGet, "/recovery",
header{
Key: "Host",
Value: "www.google.com",
},
header{
Key: "Authorization",
Value: fmt.Sprintf("Bearer %s", password),
Value: "Bearer " + password,
},
header{
Key: "Content-Type",
@@ -56,7 +55,7 @@ func TestPanicInHandler(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@@ -67,7 +66,7 @@ func TestPanicInHandler(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@@ -84,21 +83,21 @@ func TestPanicWithAbort(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, dunno, bs)
assert.Equal(t, dunnoBytes, bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, dunno, bs)
assert.Equal(t, dunnoBytes, bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
@@ -135,7 +134,7 @@ func TestPanicWithBrokenPipe(t *testing.T) {
panic(e)
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
@@ -156,7 +155,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@@ -167,7 +166,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@@ -191,7 +190,7 @@ func TestCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@@ -202,7 +201,7 @@ func TestCustomRecovery(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@@ -226,7 +225,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(router, "GET", "/recovery")
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "panic recovered")
@@ -237,7 +236,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = PerformRequest(router, "GET", "/recovery")
w = PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
@@ -246,3 +245,65 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
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)
}
})
}
}
+14 -5
View File
@@ -7,6 +7,8 @@ package render
import (
"html/template"
"net/http"
"git.company.lan/gopkg/gin/internal/fs"
)
// Delims represents a set of Left and Right delimiters for HTML template rendering.
@@ -31,10 +33,12 @@ type HTMLProduction struct {
// HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct {
Files []string
Glob string
Delims Delims
FuncMap template.FuncMap
Files []string
Glob string
FileSystem http.FileSystem
Patterns []string
Delims Delims
FuncMap template.FuncMap
}
// HTML contains template reference and its name with given interface object.
@@ -63,6 +67,7 @@ func (r HTMLDebug) Instance(name string, data any) Render {
Data: data,
}
}
func (r HTMLDebug) loadTemplate() *template.Template {
if r.FuncMap == nil {
r.FuncMap = template.FuncMap{}
@@ -73,7 +78,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
panic("the HTML debug render was created without files or glob pattern")
if r.FileSystem != nil && len(r.Patterns) > 0 {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
}
panic("the HTML debug render was created without files or glob pattern or file system with patterns")
}
// Render (HTML) executes template and writes its result with custom ContentType for response.
+16 -12
View File
@@ -9,9 +9,10 @@ import (
"fmt"
"html/template"
"net/http"
"unicode"
"git.company.lan/gopkg/gin/codec/json"
"git.company.lan/gopkg/gin/internal/bytesconv"
"git.company.lan/gopkg/gin/internal/json"
)
// JSON contains the given interface object.
@@ -65,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj any) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
jsonBytes, err := json.API.Marshal(obj)
if err != nil {
return err
}
@@ -76,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
}
@@ -92,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data)
jsonBytes, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
@@ -115,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
ret, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
@@ -151,20 +152,23 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
}
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
func (r AsciiJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
ret, err := json.API.Marshal(r.Data)
if err != nil {
return err
}
var buffer bytes.Buffer
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r)
if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r))
if r > unicode.MaxASCII {
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
buffer.Write(escapeBuf)
} else {
buffer.WriteByte(byte(r))
}
buffer.WriteString(cvt)
}
_, err = w.Write(buffer.Bytes())
@@ -179,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
encoder := json.NewEncoder(w)
encoder := json.API.NewEncoder(w)
encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data)
}
+4 -4
View File
@@ -27,7 +27,7 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
}
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers)
r.writeHeaders(w)
_, err = io.Copy(w, r.Reader)
return
}
@@ -37,10 +37,10 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}
// writeHeaders writes custom Header.
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
// writeHeaders writes headers from r.Headers into response.
func (r Reader) writeHeaders(w http.ResponseWriter) {
header := w.Header()
for k, v := range headers {
for k, v := range r.Headers {
if header.Get(k) == "" {
header.Set(k, v)
}
+57 -23
View File
@@ -15,7 +15,7 @@ import (
"strings"
"testing"
"git.company.lan/gopkg/gin/internal/json"
"git.company.lan/gopkg/gin/codec/json"
testdata "git.company.lan/gopkg/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -38,7 +38,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -60,7 +60,7 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.JSONEq(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -85,7 +85,7 @@ func TestRenderSecureJSON(t *testing.T) {
err1 := (SecureJSON{"while(1);", data}).Render(w1)
require.NoError(t, err1)
assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\"}", w1.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
@@ -173,7 +173,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
err = jsonpJSON.Render(ew)
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)
err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error())
@@ -194,7 +194,7 @@ func TestRenderJsonpJSONError2(t *testing.T) {
e := (JsonpJSON{"", data}).Render(w)
require.NoError(t, e)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -217,7 +217,7 @@ func TestRenderAsciiJSON(t *testing.T) {
err := (AsciiJSON{data1}).Render(w1)
require.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.JSONEq(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
@@ -244,7 +244,7 @@ func TestRenderPureJSON(t *testing.T) {
}
err := (PureJSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.JSONEq(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -285,7 +285,14 @@ b:
err := (YAML{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "|4-\n a : Easy!\n b:\n \tc: 2\n \td: [3, 4]\n \t\n", w.Body.String())
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3
// We're checking that the output contains the expected data, not the exact formatting
output := w.Body.String()
assert.Contains(t, output, "a : Easy!")
assert.Contains(t, output, "b:")
assert.Contains(t, output, "c: 2")
assert.Contains(t, output, "d: [3, 4]")
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -369,7 +376,7 @@ func TestRenderXML(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)
require.NoError(t, err)
data1 := Redirect{
@@ -489,10 +496,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
@@ -508,10 +517,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
Files: nil,
Glob: "../testdata/template/hello*",
FileSystem: nil,
Patterns: nil,
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
})
err := instance.Render(w)
require.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFS(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
FileSystem: http.Dir("../testdata/template"),
Patterns: []string{"hello.tmpl"},
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
@@ -526,10 +558,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{
Files: nil,
Glob: "",
Delims: Delims{"{{", "}}"},
FuncMap: nil,
Files: nil,
Glob: "",
FileSystem: nil,
Patterns: nil,
Delims: Delims{"{{", "}}"},
FuncMap: nil,
}
assert.Panics(t, func() { htmlRender.Instance("", nil) })
}
@@ -581,7 +615,7 @@ func TestRenderReaderNoContentLength(t *testing.T) {
}
func TestRenderWriteError(t *testing.T) {
data := []interface{}{"value1", "value2"}
data := []any{"value1", "value2"}
prefix := "my-prefix:"
r := SecureJSON{Data: data, Prefix: prefix}
ew := &errorWriter{
+2 -2
View File
@@ -15,7 +15,7 @@ type TOML struct {
Data any
}
var TOMLContentType = []string{"application/toml; charset=utf-8"}
var tomlContentType = []string{"application/toml; charset=utf-8"}
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
func (r TOML) Render(w http.ResponseWriter) error {
@@ -32,5 +32,5 @@ func (r TOML) Render(w http.ResponseWriter) error {
// WriteContentType (TOML) writes TOML ContentType for response.
func (r TOML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, TOMLContentType)
writeContentType(w, tomlContentType)
}
+1 -1
View File
@@ -7,7 +7,7 @@ package render
import (
"net/http"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
)
// YAML contains the given interface object.
+6
View File
@@ -6,6 +6,7 @@ package gin
import (
"bufio"
"errors"
"io"
"net"
"net/http"
@@ -16,6 +17,8 @@ const (
defaultStatus = http.StatusOK
)
var errHijackAlreadyWritten = errors.New("gin: response already written")
// ResponseWriter ...
type ResponseWriter interface {
http.ResponseWriter
@@ -106,6 +109,9 @@ func (w *responseWriter) Written() bool {
// Hijack implements the http.Hijacker interface.
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.Written() {
return nil, nil, errHijackAlreadyWritten
}
if w.size < 0 {
w.size = 0
}
+70
View File
@@ -5,6 +5,8 @@
package gin
import (
"bufio"
"net"
"net/http"
"net/http/httptest"
"testing"
@@ -124,6 +126,74 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush()
}
type mockHijacker struct {
*httptest.ResponseRecorder
hijacked bool
}
// Hijack implements the http.Hijacker interface. It just records that it was called.
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
m.hijacked = true
return nil, nil, nil
}
func TestResponseWriterHijackAfterWrite(t *testing.T) {
tests := []struct {
name string
action func(w ResponseWriter) error // Action to perform before hijacking
expectWrittenBeforeHijack bool
expectHijackSuccess bool
expectWrittenAfterHijack bool
expectError error
}{
{
name: "hijack before write should succeed",
action: func(w ResponseWriter) error { return nil },
expectWrittenBeforeHijack: false,
expectHijackSuccess: true,
expectWrittenAfterHijack: true, // Hijack itself marks the writer as written
expectError: nil,
},
{
name: "hijack after write should fail",
action: func(w ResponseWriter) error {
_, err := w.Write([]byte("test"))
return err
},
expectWrittenBeforeHijack: true,
expectHijackSuccess: false,
expectWrittenAfterHijack: true,
expectError: errHijackAlreadyWritten,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
writer := &responseWriter{}
writer.reset(hijacker)
w := ResponseWriter(writer)
// Check initial state
assert.False(t, w.Written(), "should not be written initially")
// Perform pre-hijack action
require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
// Check state before hijacking
assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
// Attempt to hijack
_, _, hijackErr := w.Hijack()
// Check results
require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
})
}
}
func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{}
+24
View File
@@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/assert"
)
var MaxHandlers = 32
func init() {
SetMode(TestMode)
}
@@ -193,3 +195,25 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.Static("/static", "."))
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
}
func TestRouterGroupCombineHandlersTooManyHandlers(t *testing.T) {
group := &RouterGroup{
Handlers: make(HandlersChain, MaxHandlers), // Assume group already has MaxHandlers middleware
}
tooManyHandlers := make(HandlersChain, MaxHandlers) // Add MaxHandlers more, total 2 * MaxHandlers
// This should trigger panic
assert.Panics(t, func() {
group.combineHandlers(tooManyHandlers)
}, "should panic due to too many handlers")
}
func TestRouterGroupCombineHandlersEmptySliceNotNil(t *testing.T) {
group := &RouterGroup{
Handlers: HandlersChain{},
}
result := group.combineHandlers(HandlersChain{})
assert.NotNil(t, result, "result should not be nil even with empty handlers")
assert.Empty(t, result, "empty handlers should return empty chain")
}
+8 -8
View File
@@ -484,7 +484,7 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Contains(t, w.Body.String(), "package gin")
// Content-Type='text/plain; charset=utf-8' when go version <= 1.16,
// else, Content-Type='text/x-go; charset=utf-8'
assert.NotEqual(t, "", w.Header().Get("Content-Type"))
assert.NotEmpty(t, w.Header().Get("Content-Type"))
assert.NotEqual(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Last-Modified"))
assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
@@ -523,8 +523,8 @@ func TestRouteNotAllowedEnabled3(t *testing.T) {
w := PerformRequest(router, http.MethodPut, "/path")
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
allowed := w.Header().Get("Allow")
assert.Contains(t, allowed, "GET")
assert.Contains(t, allowed, "POST")
assert.Contains(t, allowed, http.MethodGet)
assert.Contains(t, allowed, http.MethodPost)
}
func TestRouteNotAllowedDisabled(t *testing.T) {
@@ -557,10 +557,10 @@ func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
{"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := PerformRequest(router, "GET", tr.route)
w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
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"))
}
}
}
@@ -590,7 +590,7 @@ func TestRouterNotFound(t *testing.T) {
w := PerformRequest(router, http.MethodGet, tr.route)
assert.Equal(t, tr.code, w.Code)
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"))
}
}
@@ -764,7 +764,7 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
// Test not found
router.Use(func(c *Context) {
// For not found routes full path is empty
assert.Equal(t, "", c.FullPath())
assert.Empty(t, c.FullPath())
})
w := PerformRequest(router, http.MethodGet, "/not-found")
@@ -786,6 +786,6 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) {
v1.GET("/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)
}
+9 -2
View File
@@ -6,7 +6,10 @@ package gin
import "net/http"
// CreateTestContext returns a fresh engine and context for testing purposes
// CreateTestContext returns a fresh Engine and a Context associated with it.
// This is useful for tests that need to set up a new Gin engine instance
// along with a context, for example, to test middleware that doesn't depend on
// specific routes. The ResponseWriter `w` is used to initialize the context's writer.
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
r = New()
c = r.allocateContext(0)
@@ -15,7 +18,11 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
return
}
// CreateTestContextOnly returns a fresh context base on the engine for testing purposes
// CreateTestContextOnly returns a fresh Context associated with the provided Engine `r`.
// This is useful for tests that operate on an existing, possibly pre-configured,
// Gin engine instance and need a new context for it.
// The ResponseWriter `w` is used to initialize the context's writer.
// The context is allocated with the `maxParams` setting from the provided engine.
func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) {
c = r.allocateContext(r.maxParams)
c.reset()
+2
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.
+3 -3
View File
@@ -234,7 +234,7 @@ walk:
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
pathSeg, _, _ = strings.Cut(pathSeg, "/")
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
@@ -358,7 +358,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
pathSeg := ""
if len(n.children) != 0 {
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
}
panic("catch-all wildcard '" + path +
"' in new path '" + fullPath +
@@ -383,7 +383,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
}
n.addChild(child)
n.indices = string('/')
n.indices = "/"
n = child
n.priority++
+3 -3
View File
@@ -481,7 +481,7 @@ func TestTreeDuplicatePath(t *testing.T) {
}
}
//printChildren(tree, "")
// printChildren(tree, "")
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
@@ -532,7 +532,7 @@ func TestTreeCatchAllConflictRoot(t *testing.T) {
func TestTreeCatchMaxParams(t *testing.T) {
tree := &node{}
var route = "/cmd/*filepath"
route := "/cmd/*filepath"
tree.addRoute(route, fakeHandler(route))
}
@@ -692,7 +692,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
}
func TestRedirectTrailingSlash(t *testing.T) {
var data = []struct {
data := []struct {
path string
}{
{"/hello/:name"},
+7 -7
View File
@@ -29,7 +29,7 @@ type testStruct struct {
}
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, http.MethodPost, req.Method)
assert.Equal(t.T, "/path", req.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "hello")
@@ -39,17 +39,17 @@ func TestWrap(t *testing.T) {
router := New()
router.POST("/path", WrapH(&testStruct{t}))
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "/path2", req.URL.Path)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "hola!")
}))
w := PerformRequest(router, "POST", "/path")
w := PerformRequest(router, http.MethodPost, "/path")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String())
w = PerformRequest(router, "GET", "/path2")
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String())
}
@@ -94,7 +94,7 @@ func somefunction() {
}
func TestJoinPaths(t *testing.T) {
assert.Equal(t, "", joinPaths("", ""))
assert.Empty(t, joinPaths("", ""))
assert.Equal(t, "/", joinPaths("", "/"))
assert.Equal(t, "/a", joinPaths("/a", ""))
assert.Equal(t, "/a/", joinPaths("/a/", ""))
@@ -119,13 +119,13 @@ func TestBindMiddleware(t *testing.T) {
called = true
value = c.MustGet(BindKey).(*bindTestStruct)
})
PerformRequest(router, "GET", "/?foo=hola&bar=10")
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=10")
assert.True(t, called)
assert.Equal(t, "hola", value.Foo)
assert.Equal(t, 10, value.Bar)
called = false
PerformRequest(router, "GET", "/?foo=hola&bar=1")
PerformRequest(router, http.MethodGet, "/?foo=hola&bar=1")
assert.False(t, called)
assert.Panics(t, func() {
+1 -1
View File
@@ -5,4 +5,4 @@
package gin
// Version is the current gin framework's version.
const Version = "v1.10.0"
const Version = "v1.11.0"