From 3ab698dc5110af1977d57226e4995c57dd34c233 Mon Sep 17 00:00:00 2001 From: OHZEKI Naoki <0h23k1.n40k1@gmail.com> Date: Sat, 17 Jan 2026 17:40:43 +0900 Subject: [PATCH 01/26] refactor(recovery): smart error comparison (#4142) * refactor(recovery): rename var in CustomRecoveryWithWriter * refactor(recovery): smart error comparison * test(recovery): Directly reference the syscall error string --- recovery.go | 37 +++++++++++++++---------------------- recovery_test.go | 13 +++++++------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/recovery.go b/recovery.go index 6d4b4b2b..bbf1d565 100644 --- a/recovery.go +++ b/recovery.go @@ -12,12 +12,12 @@ import ( "fmt" "io" "log" - "net" "net/http" "net/http/httputil" "os" "runtime" "strings" + "syscall" "time" "github.com/gin-gonic/gin/internal/bytesconv" @@ -57,40 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { } return func(c *Context) { defer func() { - if err := recover(); err != nil { + if rec := recover(); rec != nil { // Check for a broken connection, as it is not really a // condition that warrants a panic stack trace. - var brokenPipe bool - if ne, ok := err.(*net.OpError); ok { - var se *os.SyscallError - if errors.As(ne, &se) { - seStr := strings.ToLower(se.Error()) - if strings.Contains(seStr, "broken pipe") || - strings.Contains(seStr, "connection reset by peer") { - brokenPipe = true - } - } - } - if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) { - brokenPipe = true + var isBrokenPipe bool + err, ok := rec.(error) + if ok { + isBrokenPipe = errors.Is(err, syscall.EPIPE) || + errors.Is(err, syscall.ECONNRESET) || + errors.Is(err, http.ErrAbortHandler) } if logger != nil { - if brokenPipe { - logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset) + if isBrokenPipe { + logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset) + timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", - timeFormat(time.Now()), err, stack(stackSkip), reset) + timeFormat(time.Now()), rec, stack(stackSkip), reset) } } - if brokenPipe { + if isBrokenPipe { // If the connection is dead, we can't write a status to it. - c.Error(err.(error)) //nolint: errcheck + c.Error(err) //nolint: errcheck c.Abort() } else { - handle(c, err) + handle(c, rec) } } }() diff --git a/recovery_test.go b/recovery_test.go index 912ab601..0faa3280 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -98,13 +98,13 @@ func TestFunction(t *testing.T) { func TestPanicWithBrokenPipe(t *testing.T) { const expectCode = 204 - expectMsgs := map[syscall.Errno]string{ - syscall.EPIPE: "broken pipe", - syscall.ECONNRESET: "connection reset by peer", + expectErrnos := []syscall.Errno{ + syscall.EPIPE, + syscall.ECONNRESET, } - for errno, expectMsg := range expectMsgs { - t.Run(expectMsg, func(t *testing.T) { + for _, errno := range expectErrnos { + t.Run("Recovery from "+errno.Error(), func(t *testing.T) { var buf strings.Builder router := New() @@ -122,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) { w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, expectCode, w.Code) - assert.Contains(t, strings.ToLower(buf.String()), expectMsg) + assert.Contains(t, strings.ToLower(buf.String()), errno.Error()) + assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]") }) } } From b2b489dbf4826c2c630717a77fd5e42774625410 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Sun, 18 Jan 2026 12:56:22 +0800 Subject: [PATCH 02/26] chore(context): always trust xff headers from unix socket (#3359) Co-authored-by: Bo-Yi Wu --- context.go | 27 ++++++++++++++++++++------- context_test.go | 10 ++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/context.go b/context.go index c42459ff..be552078 100644 --- a/context.go +++ b/context.go @@ -978,14 +978,27 @@ func (c *Context) ClientIP() string { } } - // It also checks if the remoteIP is a trusted proxy or not. - // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks - // defined by Engine.SetTrustedProxies() - remoteIP := net.ParseIP(c.RemoteIP()) - if remoteIP == nil { - return "" + var ( + trusted bool + remoteIP net.IP + ) + // If gin is listening a unix socket, always trust it. + localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr) + if ok && strings.HasPrefix(localAddr.Network(), "unix") { + trusted = true + } + + // Fallback + if !trusted { + // It also checks if the remoteIP is a trusted proxy or not. + // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks + // defined by Engine.SetTrustedProxies() + remoteIP = net.ParseIP(c.RemoteIP()) + if remoteIP == nil { + return "" + } + trusted = c.engine.isTrustedProxy(remoteIP) } - trusted := c.engine.isTrustedProxy(remoteIP) if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { for _, headerName := range c.engine.RemoteIPHeaders { diff --git a/context_test.go b/context_test.go index 3080015c..f69d574f 100644 --- a/context_test.go +++ b/context_test.go @@ -1915,6 +1915,16 @@ func TestContextClientIP(t *testing.T) { c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs() resetContextForClientIPTests(c) + // unix address + addr := &net.UnixAddr{Net: "unix", Name: "@"} + c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr)) + c.Request.RemoteAddr = addr.String() + assert.Equal(t, "20.20.20.20", c.ClientIP()) + + // reset + c.Request = c.Request.WithContext(context.Background()) + resetContextForClientIPTests(c) + // Legacy tests (validating that the defaults don't break the // (insecure!) old behaviour) assert.Equal(t, "20.20.20.20", c.ClientIP()) From 192ac89eefc1c30f7c97ae48a9ffb1c6f1c8c8bc Mon Sep 17 00:00:00 2001 From: takanuva15 <6986426+takanuva15@users.noreply.github.com> Date: Sat, 24 Jan 2026 02:20:24 -0500 Subject: [PATCH 03/26] feat(binding): add support for encoding.UnmarshalText in uri/query binding (#4203) --- binding/form_mapping.go | 68 ++++-- binding/form_mapping_test.go | 390 +++++++++++++++++++++++++++++++++++ docs/doc.md | 78 ++++++- 3 files changed, 509 insertions(+), 27 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e76e7510..db6aa0dc 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "errors" "fmt" "maps" @@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag type setOptions struct { isDefaultExists bool defaultValue string + // parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`) + parser string } func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { @@ -168,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter setOpt.defaultValue = strings.ReplaceAll(v, ";", ",") } } + } else if k, v = head(opt, "="); k == "parser" { + setOpt.parser = v } } @@ -191,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) { return false, nil } +// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field. +// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this. +func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) { + switch parser { + case "encoding.TextUnmarshaler": + v, ok := value.Addr().Interface().(encoding.TextUnmarshaler) + if !ok { + return false, nil + } + return true, v.UnmarshalText([]byte(val)) + } + return false, nil +} + func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) { cfTag := field.Tag.Get("collection_format") if cfTag == "" || cfTag == "multi" { @@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error case "pipes": sep = "|" default: - return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag) + return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag) } totalLength := 0 @@ -244,7 +263,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, err } - return true, setSlice(vs, value, field) + return true, setSlice(vs, value, field, opt) case reflect.Array: if len(vs) == 0 { if !opt.isDefaultExists { @@ -267,7 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -279,27 +302,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) } - return true, setArray(vs, value, field) + return true, setArray(vs, value, field, opt) default: var val string - if !ok { + if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") { val = opt.defaultValue + } else if len(vs) > 0 { + val = vs[0] } - if len(vs) > 0 { - val = vs[0] - if val == "" { - val = opt.defaultValue - } - } - if ok, err := trySetCustom(val, value); ok { + if ok, err = trySetUsingParser(val, value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(val, value); ok { return ok, err } - return true, setWithProperType(val, value, field) + return true, setWithProperType(val, value, field, opt) } } -func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { +func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error { + // this if-check is required for parsing nested types like []MyId, where MyId is [12]byte + if ok, err := trySetUsingParser(val, value, opt.parser); ok { + return err + } else if ok, err = trySetCustom(val, value); ok { + return err + } + // If it is a string type, no spaces are removed, and the user data is not modified here if value.Kind() != reflect.String { val = strings.TrimSpace(val) @@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel if !value.Elem().IsValid() { value.Set(reflect.New(value.Type().Elem())) } - return setWithProperType(val, value.Elem(), field) + return setWithProperType(val, value.Elem(), field, opt) default: return errUnknownType } @@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val return nil } -func setArray(vals []string, value reflect.Value, field reflect.StructField) error { +func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { for i, s := range vals { - err := setWithProperType(s, value.Index(i), field) + err := setWithProperType(s, value.Index(i), field, opt) if err != nil { return err } @@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err return nil } -func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { +func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) - err := setArray(vals, slice, field) + err := setArray(vals, slice, field, opt) if err != nil { return err } diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index e007573c..c78f7398 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "encoding/hex" "errors" "mime/multipart" @@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) { assert.EqualValues(t, 245, s.Foo) } +func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) { + var s struct { + Foo customUnmarshalParamHex `form:"foo,default=f5"` + } + err := mappingByPtr(&s, formSource{"foo": {}}, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Foo) +} + type customUnmarshalParamType struct { Protocol string Path string @@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) { assert.Equal(t, "foo", s.FileData[1]) } +func TestMappingCustomSliceStopsWhenError(t *testing.T) { + var s struct { + FileData customPath `form:"path"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUri(t *testing.T) { + var s struct { + FileData []customPath `uri:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceForm(t *testing.T) { + var s struct { + FileData []customPath `form:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + type objectID [12]byte func (o *objectID) UnmarshalParam(param string) error { @@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) { assert.Equal(t, expected, s.FileData) } +func TestMappingCustomArrayOfArrayUri(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `uri:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayForm(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `form:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +// ==== TextUnmarshaler tests START ==== + +type customUnmarshalTextHex int + +func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customUnmarshalTextHex(v) + return nil +} + +// verify type implements TextUnmarshaler +var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil) + +func TestMappingCustomUnmarshalTextHexUri(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexForm(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field1": {}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 0xf5, s.Field) +} + +type customUnmarshalTextType struct { + Protocol string + Path string + Name string +} + +func (f *customUnmarshalTextType) UnmarshalText(text []byte) error { + parts := strings.Split(string(text), ":") + if len(parts) != 3 { + return errors.New("invalid format") + } + f.Protocol = parts[0] + f.Path = parts[1] + f.Name = parts[2] + return nil +} + +var _ encoding.TextUnmarshaler = (*customUnmarshalTextType)(nil) + +func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +type customPathUnmarshalText []string + +func (p *customPathUnmarshalText) UnmarshalText(text []byte) error { + elems := strings.Split(string(text), "/") + n := len(elems) + if n < 2 { + return errors.New("invalid format") + } + + *p = elems + return nil +} + +var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil) + +func TestMappingCustomSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +type objectIDUnmarshalText [12]byte + +func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error { + oid, err := convertToOidUnmarshalText(string(text)) + if err != nil { + return err + } + + *o = oid + return nil +} + +func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) { + oid, err := convertTo(s) + return objectIDUnmarshalText(oid), err +} + +var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil) + +func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "uri") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "form") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default +// binding logic. +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalParamHex `form:"hex"` + HexByUnmarshalText customUnmarshalParamHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + "hex2": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) + assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding +} + +// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the +// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards +// compatibility) +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalTextHex `form:"hex"` + HexByUnmarshalText customUnmarshalTextHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`11`}, + "hex2": {`11`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal + assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for normal hex binding +} + +type customHexUnmarshalParamAndUnmarshalText int + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error { + return errors.New("should not be called in unit test if parser tag present") +} + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customHexUnmarshalParamAndUnmarshalText(v) + return nil +} + +// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler, +// then only the UnmarshalText method should be invoked. +func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) { + var s struct { + Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) +} + +// ==== TextUnmarshaler tests END ==== + func TestMappingEmptyValues(t *testing.T) { t.Run("slice with default", func(t *testing.T) { var s struct { diff --git a/docs/doc.md b/docs/doc.md index 0dd86684..449c8d02 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -911,7 +911,7 @@ curl -X POST http://localhost:8080/person NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply: - Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior -- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values +- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values - Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv" @@ -1009,12 +1009,68 @@ curl -v localhost:8088/thinkerou/not-uuid ### Bind custom unmarshaler +To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound. + ```go package main import ( - "github.com/gin-gonic/gin" + "encoding" "strings" + + "github.com/gin-gonic/gin" +) + +type Birthday string + +func (b *Birthday) UnmarshalText(text []byte) error { + *b = Birthday(strings.Replace(string(text), "-", "/", -1)) + return nil +} + +var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler + +func main() { + route := gin.Default() + var request struct { + Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"` + Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + route.GET("/test", func(ctx *gin.Context) { + _ = ctx.BindQuery(&request) + ctx.JSON(200, request) + }) + _ = route.Run(":8088") +} +``` + +Test it with: + +```sh +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' +``` +Result +```sh +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} +``` + +Note: +- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic. +- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client. + +--- + +If a type already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead. + +```go +package main + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) type Birthday string @@ -1024,29 +1080,37 @@ func (b *Birthday) UnmarshalParam(param string) error { return nil } +var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler + func main() { route := gin.Default() var request struct { - Birthday Birthday `form:"birthday"` + Birthday Birthday `form:"birthday"` + Birthdays []Birthday `form:"birthdays" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"` } route.GET("/test", func(ctx *gin.Context) { _ = ctx.BindQuery(&request) - ctx.JSON(200, request.Birthday) + ctx.JSON(200, request) }) - route.Run(":8088") + _ = route.Run(":8088") } ``` Test it with: ```sh -curl 'localhost:8088/test?birthday=2000-01-01' +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' ``` Result ```sh -"2000/01/01" +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} ``` +Note: +- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag. +- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client. + ### Bind Header ```go From ac95fa6bbcf5ec102bc81fbbb70e8fccba90f0b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:22:06 +0800 Subject: [PATCH 04/26] chore(deps): bump goreleaser/goreleaser-action from 5 to 6 (#3992) Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6. - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6) --- updated-dependencies: - dependency-name: goreleaser/goreleaser-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 53410d2e07054369e0960fbe2eed97e1b9966f12 Mon Sep 17 00:00:00 2001 From: Raju Ahmed <73926176+raju-mechatronics@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:54:37 +0600 Subject: [PATCH 05/26] feat(context): add GetError and GetErrorSlice methods for error retrieval (#4502) Co-authored-by: Bo-Yi Wu --- context.go | 10 ++++++++++ context_test.go | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/context.go b/context.go index be552078..c7bc61fe 100644 --- a/context.go +++ b/context.go @@ -386,6 +386,11 @@ func (c *Context) GetDuration(key any) time.Duration { return getTyped[time.Duration](c, key) } +// GetError returns the value associated with the key as an error. +func (c *Context) GetError(key any) error { + return getTyped[error](c, key) +} + // GetIntSlice returns the value associated with the key as a slice of integers. func (c *Context) GetIntSlice(key any) []int { return getTyped[[]int](c, key) @@ -451,6 +456,11 @@ func (c *Context) GetStringSlice(key any) []string { return getTyped[[]string](c, key) } +// GetErrorSlice returns the value associated with the key as a slice of errors. +func (c *Context) GetErrorSlice(key any) []error { + return getTyped[[]error](c, key) +} + // GetStringMap returns the value associated with the key as a map of interfaces. func (c *Context) GetStringMap(key any) map[string]any { return getTyped[map[string]any](c, key) diff --git a/context_test.go b/context_test.go index f69d574f..53baf6d5 100644 --- a/context_test.go +++ b/context_test.go @@ -516,6 +516,14 @@ func TestContextGetDuration(t *testing.T) { assert.Equal(t, time.Second, c.GetDuration("duration")) } +func TestContextGetError(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + key := "error" + value := errors.New("test error") + c.Set(key, value) + assert.Equal(t, value, c.GetError(key)) +} + func TestContextGetIntSlice(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) key := "int-slice" @@ -618,6 +626,14 @@ func TestContextGetStringSlice(t *testing.T) { assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice")) } +func TestContextGetErrorSlice(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + key := "error-slice" + value := []error{errors.New("error1"), errors.New("error2")} + c.Set(key, value) + assert.Equal(t, value, c.GetErrorSlice(key)) +} + func TestContextGetStringMap(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) m := make(map[string]any) From d9e5cdf9c6f9c1643be6e081516469c71645d93d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:55:09 +0800 Subject: [PATCH 06/26] chore(deps): bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1 (#4476) Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1. - [Release notes](https://github.com/goccy/go-yaml/releases) - [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1) --- updated-dependencies: - dependency-name: github.com/goccy/go-yaml dependency-version: 1.19.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3a2b2bf2..b755e40d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gin-contrib/sse v1.1.0 github.com/go-playground/validator/v10 v10.28.0 github.com/goccy/go-json v0.10.2 - github.com/goccy/go-yaml v1.19.0 + github.com/goccy/go-yaml v1.19.1 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 diff --git a/go.sum b/go.sum index a487aaaf..06442efb 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0 github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= -github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= +github.com/goccy/go-yaml v1.19.1/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= From cad29c5e3f50a9764edbfc4787825c6caabd8579 Mon Sep 17 00:00:00 2001 From: Artur Melanchyk Date: Sat, 24 Jan 2026 17:46:02 +0100 Subject: [PATCH 07/26] perf(tree): reduce allocations in findCaseInsensitivePath (#4417) Co-authored-by: Artur Melanchyk <13834276+arturmelanchyk@users.noreply.github.com> --- tree.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tree.go b/tree.go index 88f25fcb..3ac0a3b1 100644 --- a/tree.go +++ b/tree.go @@ -671,12 +671,7 @@ walk: // Outer loop for walking the tree func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) { const stackBufSize = 128 - // Use a static sized buffer on the stack in the common case. - // If the path is too long, allocate a buffer on the heap instead. - buf := make([]byte, 0, stackBufSize) - if length := len(path) + 1; length > stackBufSize { - buf = make([]byte, 0, length) - } + buf := make([]byte, 0, max(stackBufSize, len(path)+1)) ciPath := n.findCaseInsensitivePathRec( path, From e3118cc378d263454098924ebbde7e8d1dd2e904 Mon Sep 17 00:00:00 2001 From: wanghaolong613 Date: Sun, 25 Jan 2026 00:51:11 +0800 Subject: [PATCH 08/26] refactor: for loop can be modernized using range over int (#4392) Co-authored-by: Bo-Yi Wu --- benchmarks_test.go | 2 +- binding/default_validator.go | 4 ++-- binding/form_mapping.go | 2 +- context_test.go | 10 +++++----- gin_integration_test.go | 2 +- internal/bytesconv/bytesconv_test.go | 4 ++-- utils.go | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/benchmarks_test.go b/benchmarks_test.go index ca504ecb..5c5163d9 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -154,7 +154,7 @@ func runRequest(B *testing.B, r *Engine, method, path string) { w := newMockWriter() B.ReportAllocs() B.ResetTimer() - for i := 0; i < B.N; i++ { + for B.Loop() { r.ServeHTTP(w, req) } } diff --git a/binding/default_validator.go b/binding/default_validator.go index 44b7a2ac..8203bcaa 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -27,7 +27,7 @@ func (err SliceValidationError) Error() string { } var b strings.Builder - for i := 0; i < len(err); i++ { + for i := range len(err) { if err[i] != nil { if b.Len() > 0 { b.WriteString("\n") @@ -58,7 +58,7 @@ func (v *defaultValidator) ValidateStruct(obj any) error { case reflect.Slice, reflect.Array: count := value.Len() validateRet := make(SliceValidationError, 0) - for i := 0; i < count; i++ { + for i := range count { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { validateRet = append(validateRet, err) } diff --git a/binding/form_mapping.go b/binding/form_mapping.go index db6aa0dc..6982fd4f 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -119,7 +119,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag tValue := value.Type() var isSet bool - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { sf := tValue.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue diff --git a/context_test.go b/context_test.go index 53baf6d5..cb534884 100644 --- a/context_test.go +++ b/context_test.go @@ -3677,22 +3677,22 @@ func BenchmarkGetMapFromFormData(b *testing.B) { // Test case 3: Large dataset with many bracket keys largeData := make(map[string][]string) - for i := 0; i < 100; i++ { + for i := range 100 { key := fmt.Sprintf("ids[%d]", i) largeData[key] = []string{fmt.Sprintf("value%d", i)} } - for i := 0; i < 50; i++ { + for i := range 50 { key := fmt.Sprintf("names[%d]", i) largeData[key] = []string{fmt.Sprintf("name%d", i)} } - for i := 0; i < 25; i++ { + for i := range 25 { key := fmt.Sprintf("other[key%d]", i) largeData[key] = []string{fmt.Sprintf("other%d", i)} } // Test case 4: Dataset with many non-matching keys (worst case) worstCaseData := make(map[string][]string) - for i := 0; i < 100; i++ { + for i := range 100 { key := fmt.Sprintf("nonmatching%d", i) worstCaseData[key] = []string{fmt.Sprintf("value%d", i)} } @@ -3728,7 +3728,7 @@ func BenchmarkGetMapFromFormData(b *testing.B) { for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = getMapFromFormData(bm.data, bm.key) } }) diff --git a/gin_integration_test.go b/gin_integration_test.go index 3ea5fe2f..720b140f 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -400,7 +400,7 @@ func TestConcurrentHandleContext(t *testing.T) { var wg sync.WaitGroup iterations := 200 wg.Add(iterations) - for i := 0; i < iterations; i++ { + for range iterations { go func() { req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(t, err) diff --git a/internal/bytesconv/bytesconv_test.go b/internal/bytesconv/bytesconv_test.go index 60e28fb4..debfd8c2 100644 --- a/internal/bytesconv/bytesconv_test.go +++ b/internal/bytesconv/bytesconv_test.go @@ -30,7 +30,7 @@ func rawStrToBytes(s string) []byte { func TestBytesToString(t *testing.T) { data := make([]byte, 1024) - for i := 0; i < 100; i++ { + for range 100 { _, err := cRand.Read(data) if err != nil { t.Fatal(err) @@ -79,7 +79,7 @@ func RandStringBytesMaskImprSrcSB(n int) string { } func TestStringToBytes(t *testing.T) { - for i := 0; i < 100; i++ { + for range 100 { s := RandStringBytesMaskImprSrcSB(64) if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) { t.Fatal("don't match") diff --git a/utils.go b/utils.go index 62517784..2fecce46 100644 --- a/utils.go +++ b/utils.go @@ -162,7 +162,7 @@ func resolveAddress(addr []string) string { // https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters func isASCII(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] > unicode.MaxASCII { return false } From d7776de7d444935ea4385999711bd6331a98fecb Mon Sep 17 00:00:00 2001 From: Laurent Caumont <40688874+laurentcau@users.noreply.github.com> Date: Tue, 27 Jan 2026 03:09:01 +0100 Subject: [PATCH 09/26] feat(render): add bson protocol (#4145) --- binding/binding.go | 4 ++++ binding/binding_nomsgpack.go | 4 ++++ binding/binding_test.go | 16 ++++++++++++++++ binding/bson.go | 30 ++++++++++++++++++++++++++++++ context.go | 11 +++++++++++ context_test.go | 18 ++++++++++++++++++ go.mod | 13 +++++++++---- go.sum | 17 ++++++++++------- render/bson.go | 34 ++++++++++++++++++++++++++++++++++ render/render_test.go | 26 ++++++++++++++++++++++++++ 10 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 binding/bson.go create mode 100644 render/bson.go diff --git a/binding/binding.go b/binding/binding.go index 702d0e82..eced0ae2 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -23,6 +23,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -86,6 +87,7 @@ var ( Header Binding = headerBinding{} Plain BindingBody = plainBinding{} TOML BindingBody = tomlBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -110,6 +112,8 @@ func Default(method, contentType string) Binding { return TOML case MIMEMultipartPOSTForm: return FormMultipart + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go index c8e61310..ae364d79 100644 --- a/binding/binding_nomsgpack.go +++ b/binding/binding_nomsgpack.go @@ -21,6 +21,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -82,6 +83,7 @@ var ( Header = headerBinding{} TOML = tomlBinding{} Plain = plainBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -104,6 +106,8 @@ func Default(method, contentType string) Binding { return FormMultipart case MIMETOML: return TOML + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index 07619ebf..a9f8b9e3 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) + + assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON)) + assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON)) } func TestBindingJSONNilBody(t *testing.T) { @@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) { string(data), string(data[1:])) } +func TestBindingBSON(t *testing.T) { + var obj FooStruct + obj.Foo = "bar" + data, _ := bson.Marshal(&obj) + testBodyBinding(t, + BSON, "bson", + "/", "/", + string(data), + // note: for badbody, we remove first byte to make it invalid + string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) diff --git a/binding/bson.go b/binding/bson.go new file mode 100644 index 00000000..4a698247 --- /dev/null +++ b/binding/bson.go @@ -0,0 +1,30 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "io" + "net/http" + + "go.mongodb.org/mongo-driver/bson" +) + +type bsonBinding struct{} + +func (bsonBinding) Name() string { + return "bson" +} + +func (b bsonBinding) Bind(req *http.Request, obj any) error { + buf, err := io.ReadAll(req.Body) + if err == nil { + err = b.BindBody(buf, obj) + } + return err +} + +func (bsonBinding) BindBody(body []byte, obj any) error { + return bson.Unmarshal(body, obj) +} diff --git a/context.go b/context.go index c7bc61fe..d73f59e3 100644 --- a/context.go +++ b/context.go @@ -40,6 +40,7 @@ const ( MIMEYAML2 = binding.MIMEYAML2 MIMETOML = binding.MIMETOML MIMEPROTOBUF = binding.MIMEPROTOBUF + MIMEBSON = binding.MIMEBSON ) // BodyBytesKey indicates a default body bytes key. @@ -1237,6 +1238,11 @@ func (c *Context) ProtoBuf(code int, obj any) { c.Render(code, render.ProtoBuf{Data: obj}) } +// BSON serializes the given struct as BSON into the response body. +func (c *Context) BSON(code int, obj any) { + c.Render(code, render.BSON{Data: obj}) +} + // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...any) { c.Render(code, render.String{Format: format, Data: values}) @@ -1344,6 +1350,7 @@ type Negotiate struct { Data any TOMLData any PROTOBUFData any + BSONData any } // Negotiate calls different Render according to acceptable Accept format. @@ -1373,6 +1380,10 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.PROTOBUFData, config.Data) c.ProtoBuf(code, data) + case binding.MIMEBSON: + data := chooseData(config.BSONData, config.Data) + c.BSON(code, data) + default: c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck } diff --git a/context_test.go b/context_test.go index cb534884..41694585 100644 --- a/context_test.go +++ b/context_test.go @@ -32,6 +32,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -1701,6 +1702,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) { assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) } +func TestContextNegotiationWithBSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "", nil) + + c.Negotiate(http.StatusOK, Negotiate{ + Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2}, + Data: H{"foo": "bar"}, + }) + + bData, _ := bson.Marshal(H{"foo": "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, string(bData), w.Body.String()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestContextNegotiationNotSupport(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) diff --git a/go.mod b/go.mod index b755e40d..425c7a7f 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,13 @@ module github.com/gin-gonic/gin go 1.24.0 +toolchain go1.24.7 + require ( github.com/bytedance/sonic v1.14.2 github.com/gin-contrib/sse v1.1.0 github.com/go-playground/validator/v10 v10.28.0 - github.com/goccy/go-json v0.10.2 + github.com/goccy/go-json v0.10.5 github.com/goccy/go-yaml v1.19.1 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 @@ -15,10 +17,13 @@ require ( github.com/quic-go/quic-go v0.57.1 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 + go.mongodb.org/mongo-driver v1.17.7 golang.org/x/net v0.47.0 google.golang.org/protobuf v1.36.10 ) +require gopkg.in/yaml.v3 v3.0.1 // indirect + require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -30,13 +35,13 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - golang.org/x/arch v0.20.0 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 06442efb..2a6cb14c 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -41,8 +41,9 @@ 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= @@ -70,10 +71,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/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.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= diff --git a/render/bson.go b/render/bson.go new file mode 100644 index 00000000..7332b8b2 --- /dev/null +++ b/render/bson.go @@ -0,0 +1,34 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "net/http" + + "go.mongodb.org/mongo-driver/bson" +) + +// BSON contains the given interface object. +type BSON struct { + Data any +} + +var bsonContentType = []string{"application/bson"} + +// Render (BSON) marshals the given interface object and writes data with custom ContentType. +func (r BSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + + bytes, err := bson.Marshal(&r.Data) + if err == nil { + _, err = w.Write(bytes) + } + return err +} + +// WriteContentType (BSONBuf) writes BSONBuf ContentType. +func (r BSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, bsonContentType) +} diff --git a/render/render_test.go b/render/render_test.go index d9ae2067..7213e48f 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -19,6 +19,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -359,6 +360,31 @@ func TestRenderProtoBufFail(t *testing.T) { require.Error(t, err) } +func TestRenderBSON(t *testing.T) { + w := httptest.NewRecorder() + reps := []int64{int64(1), int64(2)} + type mystruct struct { + Label string + Reps []int64 + } + + data := &mystruct{ + Label: "test", + Reps: reps, + } + + (BSON{data}).WriteContentType(w) + bsonData, err := bson.Marshal(data) + require.NoError(t, err) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) + + err = (BSON{data}).Render(w) + + require.NoError(t, err) + assert.Equal(t, bsonData, w.Body.Bytes()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ From 8e07d37c63e5536eb25f4af4c91eabeee4011fba Mon Sep 17 00:00:00 2001 From: Mahan Adhikari <41227164+mahanadh@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:39:14 +0545 Subject: [PATCH 10/26] fix: Correct typos, improve documentation clarity, and remove dead code (#4511) * fix: correct typos and improve documentation clarity - Fix typo "Oupps" to "Oops" in recovery test panic messages - Fix confusing documentation in Bind() and ShouldBind() methods that incorrectly stated "JSON or XML as a JSON input" - Remove double period in StaticFileFS documentation comment - Remove unused ErrorTypeNu constant that had duplicate comment with ErrorTypeAny and was never used in the codebase * tech: Fix the pull request routing link --- CONTRIBUTING.md | 2 +- context.go | 8 ++++---- errors.go | 2 -- recovery_test.go | 26 +++++++++++++------------- routergroup.go | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9703d6b4..3b05a160 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,6 @@ Please ensure your pull request meets the following requirements: - All tests pass in available continuous integration systems (e.g., GitHub Actions). - Add or modify tests to cover your code changes. - If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README. -- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1). +- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md). Thank you for contributing! diff --git a/context.go b/context.go index d73f59e3..282fb9ac 100644 --- a/context.go +++ b/context.go @@ -751,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) @@ -832,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error { // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. func (c *Context) ShouldBind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) diff --git a/errors.go b/errors.go index 829e9d2c..c0d907b9 100644 --- a/errors.go +++ b/errors.go @@ -26,8 +26,6 @@ const ( ErrorTypePublic ErrorType = 1 << 1 // ErrorTypeAny indicates any other error. ErrorTypeAny ErrorType = 1<<64 - 1 - // ErrorTypeNu indicates any other error. - ErrorTypeNu = 2 ) // Error represents a error's specification. diff --git a/recovery_test.go b/recovery_test.go index 0faa3280..028c4ad6 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) { router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery", @@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) { router := New() router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) { router.Use(RecoveryWithWriter(nil)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") @@ -162,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) { } router.Use(CustomRecoveryWithWriter(buffer, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -181,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -197,14 +197,14 @@ func TestCustomRecovery(t *testing.T) { } router.Use(CustomRecovery(handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -216,7 +216,7 @@ func TestCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -232,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { } router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -251,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } diff --git a/routergroup.go b/routergroup.go index b2540ec1..c01b917e 100644 --- a/routergroup.go +++ b/routergroup.go @@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { }) } -// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.. +// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead. // router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false}) // Gin by default uses: gin.Dir() func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes { From 488f8c3ffa579a8d19beb2bae95ff8ef36b3d53f Mon Sep 17 00:00:00 2001 From: Varun Chawla <34209028+veeceey@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:55:23 -0800 Subject: [PATCH 11/26] refactor: replace magic numbers with named constants in bodyAllowedForStatus (#4529) Use http.StatusContinue and http.StatusOK instead of hardcoded 100 and 199 for the 1xx informational status range check, consistent with the pattern already used in logger.go. Fixes #4489 --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 282fb9ac..a00d1e55 100644 --- a/context.go +++ b/context.go @@ -1058,7 +1058,7 @@ func (c *Context) requestHeader(key string) string { // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. func bodyAllowedForStatus(status int) bool { switch { - case status >= 100 && status <= 199: + case status >= http.StatusContinue && status < http.StatusOK: return false case status == http.StatusNoContent: return false From 882f42b0ed7bbb0e5f381ea45ab8fa36638b85d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:56:24 +0800 Subject: [PATCH 12/26] chore(deps): bump golang.org/x/net from 0.47.0 to 0.49.0 (#4508) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.49.0. - [Commits](https://github.com/golang/net/compare/v0.47.0...v0.49.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 425c7a7f..1863f04c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver v1.17.7 - golang.org/x/net v0.47.0 + golang.org/x/net v0.49.0 google.golang.org/protobuf v1.36.10 ) @@ -41,7 +41,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 2a6cb14c..9b17d180 100644 --- a/go.sum +++ b/go.sum @@ -77,15 +77,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= From 71cefce08e9ee91f6fb841a668207ccfda1df169 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:56:40 +0800 Subject: [PATCH 13/26] chore(deps): bump github.com/goccy/go-yaml from 1.19.1 to 1.19.2 (#4507) Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/goccy/go-yaml/releases) - [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/goccy/go-yaml/compare/v1.19.1...v1.19.2) --- updated-dependencies: - dependency-name: github.com/goccy/go-yaml dependency-version: 1.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1863f04c..0496bc80 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gin-contrib/sse v1.1.0 github.com/go-playground/validator/v10 v10.28.0 github.com/goccy/go-json v0.10.5 - github.com/goccy/go-yaml v1.19.1 + github.com/goccy/go-yaml v1.19.2 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 diff --git a/go.sum b/go.sum index 9b17d180..f59108a6 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0 github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 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-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= -github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= From 6e3ac82fa71efda7d042b009b58215ca321f32ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:57:06 +0800 Subject: [PATCH 14/26] chore(deps): bump github.com/quic-go/quic-go from 0.57.1 to 0.59.0 (#4532) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.57.1 to 0.59.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.57.1...v0.59.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.59.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0496bc80..1bd4001f 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( 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.57.1 + github.com/quic-go/quic-go v0.59.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver v1.17.7 diff --git a/go.sum b/go.sum index f59108a6..6609e78d 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -86,8 +86,6 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From bf52b077c812e1917d17944cb6f109ab4f462ae7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:57:27 +0800 Subject: [PATCH 15/26] chore(deps): bump go.mongodb.org/mongo-driver from 1.17.7 to 1.17.9 (#4533) Bumps [go.mongodb.org/mongo-driver](https://github.com/mongodb/mongo-go-driver) from 1.17.7 to 1.17.9. - [Release notes](https://github.com/mongodb/mongo-go-driver/releases) - [Commits](https://github.com/mongodb/mongo-go-driver/compare/v1.17.7...v1.17.9) --- updated-dependencies: - dependency-name: go.mongodb.org/mongo-driver dependency-version: 1.17.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1bd4001f..42774882 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/quic-go/quic-go v0.59.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 - go.mongodb.org/mongo-driver v1.17.7 + go.mongodb.org/mongo-driver v1.17.9 golang.org/x/net v0.49.0 google.golang.org/protobuf v1.36.10 ) diff --git a/go.sum b/go.sum index 6609e78d..51355809 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= -go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= From f5c267d2f84ba8192dd9c4c67b0490abfa3dfe7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:58:17 +0800 Subject: [PATCH 16/26] chore(deps): bump aquasecurity/trivy-action in the actions group (#4534) Bumps the actions group with 1 update: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action). Updates `aquasecurity/trivy-action` from 0.33.1 to 0.34.0 - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-version: 0.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/trivy-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index b86aed7f..ec8f55ad 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Run Trivy vulnerability scanner (source code) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.0 with: scan-type: 'fs' scan-ref: '.' @@ -44,7 +44,7 @@ jobs: sarif_file: 'trivy-results.sarif' - name: Run Trivy scanner (table output for logs) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.0 if: always() with: scan-type: 'fs' From 216a4a7c283bae102c4e365662566c895dbdad50 Mon Sep 17 00:00:00 2001 From: Amirhf Date: Tue, 17 Feb 2026 20:08:36 +0330 Subject: [PATCH 17/26] test(render): add comprehensive tests for MsgPack render (#4537) * test(render): add comprehensive tests for MsgPack render * test(render): make msgpack tests deterministic Decode the rendered msgpack output and assert values instead of comparing raw bytes (which can vary with map iteration order). Enable MsgpackHandle.RawToString so msgpack strings decode as Go strings. --------- Co-authored-by: AmirHossein Fallah --- render/render_msgpack_test.go | 58 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go index 579897cc..48b23870 100644 --- a/render/render_msgpack_test.go +++ b/render/render_msgpack_test.go @@ -7,7 +7,7 @@ package render import ( - "bytes" + "errors" "net/http/httptest" "testing" @@ -16,9 +16,6 @@ import ( "github.com/ugorji/go/codec" ) -// TODO unit tests -// test errors - func TestRenderMsgPack(t *testing.T) { w := httptest.NewRecorder() data := map[string]any{ @@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) { require.NoError(t, err) - h := new(codec.MsgpackHandle) - assert.NotNil(t, h) - buf := bytes.NewBuffer([]byte{}) - assert.NotNil(t, buf) - err = codec.NewEncoder(buf, h).Encode(data) - + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) require.NoError(t, err) - assert.Equal(t, w.Body.String(), buf.String()) + assert.Equal(t, data, decoded) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) } + +func TestWriteMsgPack(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + "num": 42, + } + + err := WriteMsgPack(w, data) + require.NoError(t, err) + + assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) + + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) + require.NoError(t, err) + assert.Len(t, decoded, 2) + assert.Equal(t, "bar", decoded["foo"]) + assert.EqualValues(t, 42, decoded["num"]) +} + +type failWriter struct { + *httptest.ResponseRecorder +} + +func (w *failWriter) Write(data []byte) (int, error) { + return 0, errors.New("write error") +} + +func TestRenderMsgPackError(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + } + + err := (MsgPack{data}).Render(&failWriter{w}) + require.Error(t, err) + assert.Contains(t, err.Error(), "write error") +} From 5f424ff6f6316511b7f1598ea6dba62ea1a62eb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:39:40 +0800 Subject: [PATCH 18/26] chore(deps): bump github.com/bytedance/sonic from 1.14.2 to 1.15.0 (#4539) Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.2 to 1.15.0. - [Release notes](https://github.com/bytedance/sonic/releases) - [Commits](https://github.com/bytedance/sonic/compare/v1.14.2...v1.15.0) --- updated-dependencies: - dependency-name: github.com/bytedance/sonic dependency-version: 1.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 42774882..87570423 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.7 require ( - github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic v1.15.0 github.com/gin-contrib/sse v1.1.0 github.com/go-playground/validator/v10 v10.28.0 github.com/goccy/go-json v0.10.5 @@ -26,7 +26,7 @@ require gopkg.in/yaml.v3 v3.0.1 // indirect require ( github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect diff --git a/go.sum b/go.sum index 51355809..207dff51 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= From 5260de6a83283abb87e827130accd495ad543cf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:40:02 +0800 Subject: [PATCH 19/26] chore(deps): bump golang.org/x/net from 0.49.0 to 0.50.0 (#4538) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0. - [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 87570423..19ff4752 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.49.0 + golang.org/x/net v0.50.0 google.golang.org/protobuf v1.36.10 ) @@ -41,7 +41,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index 207dff51..ef6d6eff 100644 --- a/go.sum +++ b/go.sum @@ -77,15 +77,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 00900fb3e1ea9dde33985a0e4f6afec793d5e786 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 21 Feb 2026 22:32:32 +0800 Subject: [PATCH 20/26] ci: update CI workflows and standardize Trivy config quotes (#4531) - Update gin workflow to use v2.9 and add Go 1.26 to the matrix - Upgrade Trivy action to v0.34.0 in the scan workflow - Change all single quotes to double quotes in Trivy workflow configuration Signed-off-by: Bo-Yi Wu --- .github/workflows/gin.yml | 4 ++-- .github/workflows/trivy-scan.yml | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index 8ece7f1d..df774eab 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -26,14 +26,14 @@ jobs: - name: Setup golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.6 + version: v2.9 args: --verbose test: needs: lint strategy: matrix: os: [ubuntu-latest, macos-latest] - go: ["1.24", "1.25"] + go: ["1.24", "1.25", "1.26"] test-tags: [ "", diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index ec8f55ad..57aceb76 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -9,7 +9,7 @@ on: - master schedule: # Run daily at 00:00 UTC - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: # Allow manual trigger permissions: @@ -29,28 +29,28 @@ jobs: - name: Run Trivy vulnerability scanner (source code) uses: aquasecurity/trivy-action@0.34.0 with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - name: Upload Trivy results to GitHub Security tab uses: github/codeql-action/upload-sarif@v4 if: always() with: - sarif_file: 'trivy-results.sarif' + sarif_file: "trivy-results.sarif" - name: Run Trivy scanner (table output for logs) uses: aquasecurity/trivy-action@0.34.0 if: always() with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'table' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - exit-code: '1' + exit-code: "1" From 0c219e7902e88b27be1736d46efb241e4482b30f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:33:30 +0800 Subject: [PATCH 21/26] chore(deps): bump aquasecurity/trivy-action in the actions group (#4544) Bumps the actions group with 1 update: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action). Updates `aquasecurity/trivy-action` from 0.34.0 to 0.34.1 - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.0...0.34.1) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/trivy-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 57aceb76..a4c62bf4 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Run Trivy vulnerability scanner (source code) - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.34.1 with: scan-type: "fs" scan-ref: "." @@ -44,7 +44,7 @@ jobs: sarif_file: "trivy-results.sarif" - name: Run Trivy scanner (table output for logs) - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.34.1 if: always() with: scan-type: "fs" From 81dba468722f41347ed74ee66e9c1781d72f68a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:39:56 +0800 Subject: [PATCH 22/26] chore(deps): bump github.com/go-playground/validator/v10 (#4509) Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.28.0 to 10.30.1. - [Release notes](https://github.com/go-playground/validator/releases) - [Commits](https://github.com/go-playground/validator/compare/v10.28.0...v10.30.1) --- updated-dependencies: - dependency-name: github.com/go-playground/validator/v10 dependency-version: 10.30.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 19ff4752..459e9cdc 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.7 require ( github.com/bytedance/sonic v1.15.0 github.com/gin-contrib/sse v1.1.0 - github.com/go-playground/validator/v10 v10.28.0 + github.com/go-playground/validator/v10 v10.30.1 github.com/goccy/go-json v0.10.5 github.com/goccy/go-yaml v1.19.2 github.com/json-iterator/go v1.1.12 @@ -29,7 +29,7 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index ef6d6eff..624997b8 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -20,8 +20,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= From 1b414bd54e467cb7278de8877401af07f185edb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:18:28 +0800 Subject: [PATCH 23/26] chore(deps): bump goreleaser/goreleaser-action in the actions group (#4546) Bumps the actions group with 1 update: [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action). Updates `goreleaser/goreleaser-action` from 6 to 7 - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7) --- updated-dependencies: - dependency-name: goreleaser/goreleaser-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 0098b952..ea933e7e 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: with: go-version: "^1" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser From ba093d19477b896ac89a7fc3246af23d290b8e26 Mon Sep 17 00:00:00 2001 From: Bob Du Date: Fri, 27 Feb 2026 23:20:01 +0800 Subject: [PATCH 24/26] chore(binding): upgrade bson dependency to mongo-driver v2 (#4549) Signed-off-by: Bob Du --- binding/binding_test.go | 2 +- binding/bson.go | 2 +- context_test.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- render/bson.go | 2 +- render/render_test.go | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index a9f8b9e3..f90488cd 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,7 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) diff --git a/binding/bson.go b/binding/bson.go index 4a698247..464890f0 100644 --- a/binding/bson.go +++ b/binding/bson.go @@ -8,7 +8,7 @@ import ( "io" "net/http" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) type bsonBinding struct{} diff --git a/context_test.go b/context_test.go index 41694585..41ec7bd5 100644 --- a/context_test.go +++ b/context_test.go @@ -32,7 +32,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) diff --git a/go.mod b/go.mod index 459e9cdc..0f278049 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/quic-go/quic-go v0.59.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 - go.mongodb.org/mongo-driver v1.17.9 + go.mongodb.org/mongo-driver/v2 v2.5.0 golang.org/x/net v0.50.0 google.golang.org/protobuf v1.36.10 ) diff --git a/go.sum b/go.sum index 624997b8..71c07795 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= -go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= diff --git a/render/bson.go b/render/bson.go index 7332b8b2..07f02333 100644 --- a/render/bson.go +++ b/render/bson.go @@ -7,7 +7,7 @@ package render import ( "net/http" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" ) // BSON contains the given interface object. diff --git a/render/render_test.go b/render/render_test.go index 7213e48f..40e7e7be 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -19,7 +19,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) From db309081bc5c137b2aa15701ef53f7f19788da25 Mon Sep 17 00:00:00 2001 From: Jacob McSwain Date: Fri, 27 Feb 2026 09:33:46 -0600 Subject: [PATCH 25/26] chore(logger): allow skipping query string output (#4547) This is useful for APIs that might have sensitive information in the query string, such as API keys. This patch does not change the default behavior of the code unless the new `SkipQueryString` config option is passed in. The "skip" term is a bit of a misnomer here, as this doesn't actually skip that log, but modifies the output. I'm open to suggestions for a more appropriate name. Co-authored-by: Bo-Yi Wu --- docs/doc.md | 15 +++++++++++++++ logger.go | 7 ++++++- logger_test.go | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/doc.md b/docs/doc.md index 449c8d02..7201df5c 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -22,6 +22,7 @@ - [How to write log file](#how-to-write-log-file) - [Custom Log Format](#custom-log-format) - [Controlling Log output coloring](#controlling-log-output-coloring) + - [Avoid logging query strings](#avoid-loging-query-strings) - [Model binding and validation](#model-binding-and-validation) - [Custom Validators](#custom-validators) - [Only Bind Query String](#only-bind-query-string) @@ -592,6 +593,20 @@ func main() { } ``` +### Avoid logging query strings + +```go +func main() { + router := gin.New() + + // SkipQueryString indicates that the logger should not log the query string. + // For example, /path?q=1 will be logged as /path + loggerConfig := gin.LoggerConfig{SkipQueryString: true} + + router.Use(gin.LoggerWithConfig(loggerConfig)) +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz). diff --git a/logger.go b/logger.go index 6441f7ea..cf92553a 100644 --- a/logger.go +++ b/logger.go @@ -48,6 +48,11 @@ type LoggerConfig struct { // Optional. SkipPaths []string + // SkipQueryString indicates that query strings should not be written + // for cases such as when API keys are passed via query strings. + // Optional. Default value is false. + SkipQueryString bool + // Skip is a Skipper that indicates which logs should not be written. // Optional. Skip Skipper @@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.BodySize = c.Writer.Size() - if raw != "" { + if raw != "" && !conf.SkipQueryString { path = path + "?" + raw } diff --git a/logger_test.go b/logger_test.go index 53d0df95..8099a894 100644 --- a/logger_test.go +++ b/logger_test.go @@ -471,3 +471,17 @@ func TestForceConsoleColor(t *testing.T) { // reset console color mode. consoleColorMode = autoColor } + +func TestLoggerWithConfigSkipQueryString(t *testing.T) { + buffer := new(strings.Builder) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + SkipQueryString: true, + })) + router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) }) + + PerformRequest(router, "GET", "/logged?a=21") + assert.Contains(t, buffer.String(), "200") + assert.NotContains(t, buffer.String(), "a=21") +} From 5c00df8afadd06cc5be530dde00fe6d9fa4a2e4a Mon Sep 17 00:00:00 2001 From: Denis Galeev <11516397+dengaleev@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:07:31 +0700 Subject: [PATCH 26/26] fix(render): write content length in Data.Render (#4206) * init test * fix test * fix assert.EqualValues usage --------- Co-authored-by: Bo-Yi Wu --- render/data.go | 8 +++++++- render/render_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/render/data.go b/render/data.go index a653ea30..2c0ad5e3 100644 --- a/render/data.go +++ b/render/data.go @@ -4,7 +4,10 @@ package render -import "net/http" +import ( + "net/http" + "strconv" +) // Data contains ContentType and bytes data. type Data struct { @@ -15,6 +18,9 @@ type Data struct { // Render (Data) writes data with custom ContentType. func (r Data) Render(w http.ResponseWriter) (err error) { r.WriteContentType(w) + if len(r.Data) > 0 { + w.Header().Set("Content-Length", strconv.Itoa(len(r.Data))) + } _, err = w.Write(r.Data) return } diff --git a/render/render_test.go b/render/render_test.go index 40e7e7be..9c3019eb 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -8,6 +8,7 @@ import ( "encoding/xml" "errors" "html/template" + "io" "net" "net/http" "net/http/httptest" @@ -453,6 +454,36 @@ func TestRenderData(t *testing.T) { require.NoError(t, err) assert.Equal(t, "#!PNG some raw data", w.Body.String()) assert.Equal(t, "image/png", w.Header().Get("Content-Type")) + assert.Equal(t, "19", w.Header().Get("Content-Length")) +} + +func TestRenderDataContentLength(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + size, err := strconv.Atoi(r.URL.Query().Get("size")) + assert.NoError(t, err) + + data := Data{ + ContentType: "application/octet-stream", + Data: make([]byte, size), + } + assert.NoError(t, data.Render(w)) + })) + t.Cleanup(srv.Close) + + for _, size := range []int{0, 1, 100, 100_000} { + t.Run(strconv.Itoa(size), func(t *testing.T) { + resp, err := http.Get(srv.URL + "?size=" + strconv.Itoa(size)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type")) + assert.Equal(t, strconv.Itoa(size), resp.Header.Get("Content-Length")) + + actual, err := io.Copy(io.Discard, resp.Body) + require.NoError(t, err) + assert.EqualValues(t, size, actual) + }) + } } func TestRenderString(t *testing.T) {