From 87c207a14093666fae281e9ebabe3ce6dd0b5ecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:02:56 +0800 Subject: [PATCH 01/28] chore(deps): bump github.com/bytedance/sonic from 1.14.0 to 1.14.2 (#4410) Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.0 to 1.14.2. - [Release notes](https://github.com/bytedance/sonic/releases) - [Commits](https://github.com/bytedance/sonic/compare/v1.14.0...v1.14.2) --- updated-dependencies: - dependency-name: github.com/bytedance/sonic dependency-version: 1.14.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 5 +++-- go.sum | 14 +++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 961916f0..e3a9ed25 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gin-gonic/gin go 1.24.0 require ( - github.com/bytedance/sonic v1.14.0 + 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 @@ -20,7 +20,8 @@ require ( ) require ( - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic/loader v0.4.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 2dfb4d75..31702f3a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -49,10 +51,12 @@ github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= From 52f70cf18a61939ab25696fa335ecb8934512fb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:03:29 +0800 Subject: [PATCH 02/28] chore(deps): bump github.com/ugorji/go/codec from 1.3.0 to 1.3.1 (#4409) Bumps [github.com/ugorji/go/codec](https://github.com/ugorji/go) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/ugorji/go/releases) - [Commits](https://github.com/ugorji/go/compare/codec/v1.3.0...codec/v1.3.1) --- updated-dependencies: - dependency-name: github.com/ugorji/go/codec dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 e3a9ed25..beabc954 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/quic-go/quic-go v0.55.0 github.com/stretchr/testify v1.11.1 - github.com/ugorji/go/codec v1.3.0 + github.com/ugorji/go/codec v1.3.1 golang.org/x/net v0.46.0 google.golang.org/protobuf v1.36.10 ) diff --git a/go.sum b/go.sum index 31702f3a..ed1361a9 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +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= From 2e22e5085960205fbb11c25776f6ea76b8053253 Mon Sep 17 00:00:00 2001 From: Name <1911860538@qq.com> Date: Fri, 31 Oct 2025 22:09:07 +0800 Subject: [PATCH 03/28] perf(tree): optimize path parsing using strings.Count (#4246) Co-authored-by: 1911860538 --- tree.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tree.go b/tree.go index 78479b6f..bcc83502 100644 --- a/tree.go +++ b/tree.go @@ -5,7 +5,6 @@ package gin import ( - "bytes" "net/url" "strings" "unicode" @@ -14,12 +13,6 @@ import ( "github.com/gin-gonic/gin/internal/bytesconv" ) -var ( - strColon = []byte(":") - strStar = []byte("*") - strSlash = []byte("/") -) - // Param is a single URL parameter, consisting of a key and a value. type Param struct { Key string @@ -85,16 +78,13 @@ func (n *node) addChild(child *node) { } func countParams(path string) uint16 { - var n uint16 - s := bytesconv.StringToBytes(path) - n += uint16(bytes.Count(s, strColon)) - n += uint16(bytes.Count(s, strStar)) - return n + colons := strings.Count(path, ":") + stars := strings.Count(path, "*") + return uint16(colons + stars) } func countSections(path string) uint16 { - s := bytesconv.StringToBytes(path) - return uint16(bytes.Count(s, strSlash)) + return uint16(strings.Count(path, "/")) } type nodeType uint8 From 5e5ff3ace496a31b138b0820136a146bfb5de0ef Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Thu, 6 Nov 2025 14:15:50 +0800 Subject: [PATCH 04/28] ci: replace vulnerability scanning workflow with Trivy integration (#4421) - Remove the vulnerability-scanning job from the gin workflow - Add a dedicated Trivy security scan workflow with scheduled, push, pull request, and manual triggers - Improve Trivy scan output by uploading SARIF results to the GitHub Security tab and logging table output Signed-off-by: Bo-Yi Wu --- .github/workflows/gin.yml | 16 --------- .github/workflows/trivy-scan.yml | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/trivy-scan.yml diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index f61c6486..eb0d7c26 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -81,19 +81,3 @@ jobs: uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }} - - vulnerability-scanning: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Run Trivy vulnerability scanner in repo mode - uses: aquasecurity/trivy-action@0.33.1 - with: - scan-type: "fs" - ignore-unfixed: true - format: "table" - exit-code: "1" - severity: "CRITICAL,HIGH,MEDIUM" diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 00000000..c2e29f07 --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,57 @@ +name: Trivy Security Scan + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + # Run every 3 months (quarterly) on the 1st day at 00:00 UTC + # Months: January (1), April (4), July (7), October (10) + - cron: '0 0 1 1,4,7,10 *' + workflow_dispatch: # Allow manual trigger + +permissions: + contents: read + security-events: write # Required for uploading SARIF results + +jobs: + trivy-scan: + name: Trivy Security Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Run Trivy vulnerability scanner (source code) + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'vuln,secret,misconfig' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH,MEDIUM' + ignore-unfixed: true + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + - name: Run Trivy scanner (table output for logs) + uses: aquasecurity/trivy-action@0.33.1 + if: always() + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'vuln,secret,misconfig' + format: 'table' + severity: 'CRITICAL,HIGH,MEDIUM' + ignore-unfixed: true + exit-code: '1' From dceb61e6e76337b388109f6c553b026d3b6ff026 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Fri, 7 Nov 2025 11:57:12 +0800 Subject: [PATCH 05/28] docs(README): add a Trivy security scan badge (#4426) - Add a Trivy security scan badge to the documentation - Import the log package in the example code - Improve error handling for server startup in the example code Signed-off-by: Bo-Yi Wu --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 629cb98d..1b9ab808 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status](https://github.com/gin-gonic/gin/actions/workflows/gin.yml/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions/workflows/gin.yml) +[![Trivy Security Scan](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc) @@ -62,6 +63,7 @@ Here's a complete example that demonstrates Gin's simplicity: package main import ( + "log" "net/http" "github.com/gin-gonic/gin" @@ -70,7 +72,7 @@ import ( func main() { // Create a Gin router with default middleware (logger and recovery) r := gin.Default() - + // Define a simple GET endpoint r.GET("/ping", func(c *gin.Context) { // Return JSON response @@ -78,10 +80,12 @@ func main() { "message": "pong", }) }) - + // Start server on port 8080 (default) // Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows) - r.Run() + if err := r.Run(); err != nil { + log.Fatalf("failed to run server: %v", err) + } } ``` @@ -190,7 +194,6 @@ Gin has a rich ecosystem of middleware for common web development needs. Explore - CORS, Rate limiting, Compression - Logging, Metrics, Tracing - Static file serving, Template engines - - **[gin-gonic/contrib](https://github.com/gin-gonic/contrib)** - Additional community middleware ## 🏢 Production Usage From 0c0e99d2538609d38c757b0a32f708b4dcf424c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:57:41 +0800 Subject: [PATCH 06/28] chore(deps): bump github/codeql-action from 3 to 4 in the actions group (#4425) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3 to 4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/trivy-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index c2e29f07..12830633 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -39,7 +39,7 @@ jobs: ignore-unfixed: true - name: Upload Trivy results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: 'trivy-results.sarif' From acc55e049e33b401e810dbd8c0d6dcb6b3ba2b05 Mon Sep 17 00:00:00 2001 From: Name <1911860538@qq.com> Date: Fri, 7 Nov 2025 11:59:58 +0800 Subject: [PATCH 07/28] feat(context): add Protocol Buffers support to content negotiation (#4423) Co-authored-by: 1911860538 --- context.go | 22 ++++++++++++++-------- context_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index e64c7953..d5ef8b81 100644 --- a/context.go +++ b/context.go @@ -39,6 +39,7 @@ const ( MIMEYAML = binding.MIMEYAML MIMEYAML2 = binding.MIMEYAML2 MIMETOML = binding.MIMETOML + MIMEPROTOBUF = binding.MIMEPROTOBUF ) // BodyBytesKey indicates a default body bytes key. @@ -1280,14 +1281,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool { // Negotiate contains all negotiations data. type Negotiate struct { - Offered []string - HTMLName string - HTMLData any - JSONData any - XMLData any - YAMLData any - Data any - TOMLData any + Offered []string + HTMLName string + HTMLData any + JSONData any + XMLData any + YAMLData any + Data any + TOMLData any + PROTOBUFData any } // Negotiate calls different Render according to acceptable Accept format. @@ -1313,6 +1315,10 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.TOMLData, config.Data) c.TOML(code, data) + case binding.MIMEPROTOBUF: + data := chooseData(config.PROTOBUFData, config.Data) + c.ProtoBuf(code, data) + 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 e6b7519e..26106129 100644 --- a/context_test.go +++ b/context_test.go @@ -1628,6 +1628,32 @@ func TestContextNegotiationWithHTML(t *testing.T) { assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } +func TestContextNegotiationWithPROTOBUF(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/", nil) + + reps := []int64{int64(1), int64(2)} + label := "test" + data := &testdata.Test{ + Label: &label, + Reps: reps, + } + + c.Negotiate(http.StatusCreated, Negotiate{ + Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML}, + Data: data, + }) + + // Marshal original data for comparison + protoData, err := proto.Marshal(data) + require.NoError(t, err) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, string(protoData), w.Body.String()) + assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) +} + func TestContextNegotiationNotSupport(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) From c3d5a28ed6d3849da820195b6774d212bcc038a9 Mon Sep 17 00:00:00 2001 From: Name <1911860538@qq.com> Date: Fri, 7 Nov 2025 12:01:19 +0800 Subject: [PATCH 08/28] fix(gin): close os.File in RunFd to prevent resource leak (#4422) Co-authored-by: 1911860538 --- gin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gin.go b/gin.go index 1965a429..38361a4b 100644 --- a/gin.go +++ b/gin.go @@ -593,6 +593,7 @@ func (engine *Engine) RunFd(fd int) (err error) { } f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd)) + defer f.Close() listener, err := net.FileListener(f) if err != nil { return From d1bcabc7ee4cbd3631c71f5a25da14bf1b84a0d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:32:23 +0800 Subject: [PATCH 09/28] chore(deps): bump golangci/golangci-lint-action in the actions group (#4431) Bumps the actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action). Updates `golangci/golangci-lint-action` from 8 to 9 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index eb0d7c26..d74a8bb4 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -24,7 +24,7 @@ jobs: with: go-version: "^1" - name: Setup golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: version: v2.1.6 args: --verbose From a9401cd238378d6ecaf4fe90f7c825f624bd8ea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:32:48 +0800 Subject: [PATCH 10/28] chore(deps): bump github.com/quic-go/quic-go from 0.55.0 to 0.56.0 (#4430) Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.55.0 to 0.56.0. - [Release notes](https://github.com/quic-go/quic-go/releases) - [Commits](https://github.com/quic-go/quic-go/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: github.com/quic-go/quic-go dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 ++---- go.sum | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index beabc954..ab2fc86a 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,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.55.0 + github.com/quic-go/quic-go v0.56.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 golang.org/x/net v0.46.0 @@ -28,6 +28,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -35,10 +36,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.43.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/tools v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ed1361a9..5650d5c9 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -32,6 +33,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -46,8 +51,10 @@ 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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= -github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -69,23 +76,20 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 19c2d5c0d1d096e1014fb7be62116ee9025d0f56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:42:25 +0800 Subject: [PATCH 11/28] chore(deps): bump golang.org/x/net from 0.46.0 to 0.47.0 (#4433) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.46.0 to 0.47.0. - [Commits](https://github.com/golang/net/compare/v0.46.0...v0.47.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 ab2fc86a..c756803a 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/quic-go/quic-go v0.56.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 - golang.org/x/net v0.46.0 + golang.org/x/net v0.47.0 google.golang.org/protobuf v1.36.10 ) @@ -35,8 +35,8 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/crypto v0.44.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 5650d5c9..1ef1ad18 100644 --- a/go.sum +++ b/go.sum @@ -74,15 +74,15 @@ 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= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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/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 fb27ef26c2fdfe25344b4c039d8a53551f9e912c Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 15 Nov 2025 19:21:42 +0800 Subject: [PATCH 12/28] ci(lint): refactor test assertions and linter configuration (#4436) - Update golangci-lint GitHub Action version from v2.1.6 to v2.6 - Remove the gci formatter and exclusions for third_party, builtin, and examples from the linter config - Fix argument order for assert.EqualValues and assert.Exactly in context tests for clarity - Refactor integration tests to build response strings using strings.Builder instead of direct concatenation for improved performance and readability Signed-off-by: appleboy --- .github/workflows/gin.yml | 2 +- .golangci.yml | 4 ---- context_test.go | 4 ++-- gin_integration_test.go | 16 ++++++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index d74a8bb4..8bca364d 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -26,7 +26,7 @@ jobs: - name: Setup golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.1.6 + version: v2.6 args: --verbose test: needs: lint diff --git a/.golangci.yml b/.golangci.yml index d8887062..318eb811 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -68,7 +68,6 @@ linters: - examples$ formatters: enable: - - gci - gofmt - gofumpt - goimports @@ -80,7 +79,4 @@ formatters: exclusions: generated: lax paths: - - third_party$ - - builtin$ - - examples$ - gin.go diff --git a/context_test.go b/context_test.go index 26106129..126646fc 100644 --- a/context_test.go +++ b/context_test.go @@ -292,7 +292,7 @@ func TestContextReset(t *testing.T) { assert.Empty(t, c.Errors.Errors()) assert.Empty(t, c.Errors.ByType(ErrorTypeAny)) assert.Empty(t, c.Params) - assert.EqualValues(t, c.index, -1) + assert.EqualValues(t, -1, c.index) assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) } @@ -384,7 +384,7 @@ func TestContextSetGetValues(t *testing.T) { c.Set("intInterface", a) assert.Exactly(t, "this is a string", c.MustGet("string").(string)) - assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) + assert.Exactly(t, int32(-42), c.MustGet("int32").(int32)) assert.Exactly(t, int64(42424242424242), c.MustGet("int64").(int64)) assert.Exactly(t, uint64(42), c.MustGet("uint64").(uint64)) assert.InDelta(t, float32(4.2), c.MustGet("float32").(float32), 0.01) diff --git a/gin_integration_test.go b/gin_integration_test.go index c032d837..e040993a 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -16,6 +16,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "sync" "testing" "time" @@ -261,10 +262,11 @@ func TestUnixSocket(t *testing.T) { fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n") scanner := bufio.NewScanner(c) - var response string + var responseBuilder strings.Builder for scanner.Scan() { - response += scanner.Text() + responseBuilder.WriteString(scanner.Text()) } + response := responseBuilder.String() assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "it worked", "resp body should match") } @@ -322,10 +324,11 @@ func TestFileDescriptor(t *testing.T) { fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") scanner := bufio.NewScanner(c) - var response string + var responseBuilder strings.Builder for scanner.Scan() { - response += scanner.Text() + responseBuilder.WriteString(scanner.Text()) } + response := responseBuilder.String() assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "it worked", "resp body should match") } @@ -354,10 +357,11 @@ func TestListener(t *testing.T) { fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") scanner := bufio.NewScanner(c) - var response string + var responseBuilder strings.Builder for scanner.Scan() { - response += scanner.Text() + responseBuilder.WriteString(scanner.Text()) } + response := responseBuilder.String() assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") assert.Contains(t, response, "it worked", "resp body should match") } From a85ef5ce4d0cda8834c59c855068ed48b51192d1 Mon Sep 17 00:00:00 2001 From: efcking Date: Sat, 15 Nov 2025 19:22:18 +0800 Subject: [PATCH 13/28] refactor: use b.Loop() to simplify the code and improve performance (#4432) Signed-off-by: efcking --- binding/default_validator_benchmark_test.go | 3 +-- binding/form_mapping_benchmark_test.go | 4 ++-- path_test.go | 6 +++--- utils_test.go | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/binding/default_validator_benchmark_test.go b/binding/default_validator_benchmark_test.go index 44547412..a7b22696 100644 --- a/binding/default_validator_benchmark_test.go +++ b/binding/default_validator_benchmark_test.go @@ -18,9 +18,8 @@ func BenchmarkSliceValidationError(b *testing.B) { } b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { if len(e.Error()) == 0 { b.Errorf("error") } diff --git a/binding/form_mapping_benchmark_test.go b/binding/form_mapping_benchmark_test.go index 5788133f..d40699e9 100644 --- a/binding/form_mapping_benchmark_test.go +++ b/binding/form_mapping_benchmark_test.go @@ -31,7 +31,7 @@ type structFull struct { func BenchmarkMapFormFull(b *testing.B) { var s structFull - for i := 0; i < b.N; i++ { + for b.Loop() { err := mapForm(&s, form) if err != nil { b.Fatalf("Error on a form mapping") @@ -54,7 +54,7 @@ type structName struct { func BenchmarkMapFormName(b *testing.B) { var s structName - for i := 0; i < b.N; i++ { + for b.Loop() { err := mapForm(&s, form) if err != nil { b.Fatalf("Error on a form mapping") diff --git a/path_test.go b/path_test.go index 2269b78e..7d86086f 100644 --- a/path_test.go +++ b/path_test.go @@ -94,7 +94,7 @@ func TestPathCleanMallocs(t *testing.T) { func BenchmarkPathClean(b *testing.B) { b.ReportAllocs() - for i := 0; i < b.N; i++ { + for b.Loop() { for _, test := range cleanTests { cleanPath(test.path) } @@ -134,10 +134,10 @@ func TestPathCleanLong(t *testing.T) { func BenchmarkPathCleanLong(b *testing.B) { cleanTests := genLongPaths() - b.ResetTimer() + b.ReportAllocs() - for i := 0; i < b.N; i++ { + for b.Loop() { for _, test := range cleanTests { cleanPath(test.path) } diff --git a/utils_test.go b/utils_test.go index dc9886d7..8bcf00e4 100644 --- a/utils_test.go +++ b/utils_test.go @@ -19,7 +19,7 @@ func init() { } func BenchmarkParseAccept(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") } } From 58135f06cf206a9ff713eb14150ef04a05b031d4 Mon Sep 17 00:00:00 2001 From: AtoriUzawa <110576658+AtoriUzawa@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:46:45 +0800 Subject: [PATCH 14/28] docs(context): add example comments for ShouldBind* methods (#4428) - Added detailed example for ShouldBindJSON - Added consistent descriptive comments for ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindTOML, ShouldBindPlain, ShouldBindHeader, ShouldBindUri - Makes binding method usage clearer for new users --- context.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/context.go b/context.go index d5ef8b81..059e85a8 100644 --- a/context.go +++ b/context.go @@ -830,41 +830,71 @@ func (c *Context) ShouldBind(obj any) error { } // ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). +// +// Example: +// +// POST /user +// Content-Type: application/json +// +// Request Body: +// { +// "name": "Manu", +// "age": 20 +// } +// +// type User struct { +// Name string `json:"name"` +// Age int `json:"age"` +// } +// +// var user User +// if err := c.ShouldBindJSON(&user); err != nil { +// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) +// return +// } +// c.JSON(http.StatusOK, user) func (c *Context) ShouldBindJSON(obj any) error { return c.ShouldBindWith(obj, binding.JSON) } // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). +// It works like ShouldBindJSON but binds the request body as XML data. func (c *Context) ShouldBindXML(obj any) error { return c.ShouldBindWith(obj, binding.XML) } // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). +// It works like ShouldBindJSON but binds query parameters from the URL. func (c *Context) ShouldBindQuery(obj any) error { return c.ShouldBindWith(obj, binding.Query) } // ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). +// It works like ShouldBindJSON but binds the request body as YAML data. func (c *Context) ShouldBindYAML(obj any) error { return c.ShouldBindWith(obj, binding.YAML) } // ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). +// It works like ShouldBindJSON but binds the request body as TOML data. func (c *Context) ShouldBindTOML(obj any) error { return c.ShouldBindWith(obj, binding.TOML) } // ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain). +// It works like ShouldBindJSON but binds plain text data from the request body. func (c *Context) ShouldBindPlain(obj any) error { return c.ShouldBindWith(obj, binding.Plain) } // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). +// It works like ShouldBindJSON but binds values from HTTP headers. func (c *Context) ShouldBindHeader(obj any) error { return c.ShouldBindWith(obj, binding.Header) } // ShouldBindUri binds the passed struct pointer using the specified binding engine. +// It works like ShouldBindJSON but binds parameters from the URI. func (c *Context) ShouldBindUri(obj any) error { m := make(map[string][]string, len(c.Params)) for _, v := range c.Params { From 93ff771e6dbf10e432864b30f3719ac5c84a4d4a Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 15 Nov 2025 23:03:32 +0800 Subject: [PATCH 15/28] ci(sec): improve type safety and server organization in HTTP middleware (#4437) - Update linting configuration to exclude G115 gosec check instead of including specific checks - Add the safeInt8 helper for safer type conversions and use it to prevent int8 overflow in middleware handler execution - Group related constants and variables together for better organization in gin.go - Refactor HTTP server instantiation to use a dedicated http.Server object for all Run methods - Add the safeUint16 helper and use it to safely handle conversions in tree node functions to prevent uint16 overflow Signed-off-by: appleboy --- .golangci.yml | 11 ++--------- context.go | 10 +++++++++- gin.go | 38 ++++++++++++++++++++++++++++---------- tree.go | 13 +++++++++++-- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 318eb811..f0898565 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,15 +18,8 @@ linters: - wastedassign settings: gosec: - includes: - - G102 - - G106 - - G108 - - G109 - - G111 - - G112 - - G201 - - G203 + excludes: + - G115 perfsprint: int-conversion: true err-error: true diff --git a/context.go b/context.go index 059e85a8..112f0ee0 100644 --- a/context.go +++ b/context.go @@ -55,6 +55,14 @@ const ContextRequestKey ContextKeyType = 0 // abortIndex represents a typical value used in abort functions. const abortIndex int8 = math.MaxInt8 >> 1 +// safeInt8 converts int to int8 safely, capping at math.MaxInt8 +func safeInt8(n int) int8 { + if n > math.MaxInt8 { + return math.MaxInt8 + } + return int8(n) +} + // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { @@ -186,7 +194,7 @@ func (c *Context) FullPath() string { // See example in GitHub. func (c *Context) Next() { c.index++ - for c.index < int8(len(c.handlers)) { + for c.index < safeInt8(len(c.handlers)) { if c.handlers[c.index] != nil { c.handlers[c.index](c) } diff --git a/gin.go b/gin.go index 38361a4b..4d0c7ec0 100644 --- a/gin.go +++ b/gin.go @@ -23,10 +23,12 @@ import ( "golang.org/x/net/http2/h2c" ) -const defaultMultipartMemory = 32 << 20 // 32 MB -const escapedColon = "\\:" -const colon = ":" -const backslash = "\\" +const ( + defaultMultipartMemory = 32 << 20 // 32 MB + escapedColon = "\\:" + colon = ":" + backslash = "\\" +) var ( default404Body = []byte("404 page not found") @@ -46,8 +48,10 @@ var defaultTrustedCIDRs = []*net.IPNet{ }, } -var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+") -var regRemoveRepeatedChar = regexp.MustCompile("/{2,}") +var ( + regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+") + regRemoveRepeatedChar = regexp.MustCompile("/{2,}") +) // HandlerFunc defines the handler used by gin middleware as return value. type HandlerFunc func(*Context) @@ -537,7 +541,11 @@ func (engine *Engine) Run(addr ...string) (err error) { engine.updateRouteTrees() address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) - err = http.ListenAndServe(address, engine.Handler()) + server := &http.Server{ // #nosec G112 + Addr: address, + Handler: engine.Handler(), + } + err = server.ListenAndServe() return } @@ -553,7 +561,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) { "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") } - err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler()) + server := &http.Server{ // #nosec G112 + Addr: addr, + Handler: engine.Handler(), + } + err = server.ListenAndServeTLS(certFile, keyFile) return } @@ -576,7 +588,10 @@ func (engine *Engine) RunUnix(file string) (err error) { defer listener.Close() defer os.Remove(file) - err = http.Serve(listener, engine.Handler()) + server := &http.Server{ // #nosec G112 + Handler: engine.Handler(), + } + err = server.Serve(listener) return } @@ -630,7 +645,10 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) { "Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.") } - err = http.Serve(listener, engine.Handler()) + server := &http.Server{ // #nosec G112 + Handler: engine.Handler(), + } + err = server.Serve(listener) return } diff --git a/tree.go b/tree.go index bcc83502..eff07734 100644 --- a/tree.go +++ b/tree.go @@ -5,6 +5,7 @@ package gin import ( + "math" "net/url" "strings" "unicode" @@ -77,14 +78,22 @@ func (n *node) addChild(child *node) { } } +// safeUint16 converts int to uint16 safely, capping at math.MaxUint16 +func safeUint16(n int) uint16 { + if n > math.MaxUint16 { + return math.MaxUint16 + } + return uint16(n) +} + func countParams(path string) uint16 { colons := strings.Count(path, ":") stars := strings.Count(path, "*") - return uint16(colons + stars) + return safeUint16(colons + stars) } func countSections(path string) uint16 { - return uint16(strings.Count(path, "/")) + return safeUint16(strings.Count(path, "/")) } type nodeType uint8 From 5fad976b372e381312f8de69f0969f1284d229d3 Mon Sep 17 00:00:00 2001 From: Pawan Kalyan <91543630+pawannn@users.noreply.github.com> Date: Sun, 16 Nov 2025 06:52:07 +0530 Subject: [PATCH 16/28] fix(gin): literal colon routes not working with engine.Handler() (#4415) * fix: call updateRouteTrees in ServeHTTP using sync.Once to support literal colon routes in all usage scenarios (#4413) * chore: fixed golangci-lint issue in test cases for literal colon * fix: gofumpt formatting issue * fix: gofumpt issue in gin.go * chore: updated routeTreesUpdated comments * chore: removed unused variable and updated TestUpdateRouteTreesCalledOnce testcase * chore: moved tests from literal_colon_test.go into gin_test.go --------- Co-authored-by: pawannn --- gin.go | 8 +++++ gin_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/gin.go b/gin.go index 4d0c7ec0..d71086d1 100644 --- a/gin.go +++ b/gin.go @@ -98,6 +98,10 @@ const ( type Engine struct { RouterGroup + // routeTreesUpdated ensures that the initialization or update of the route trees + // (used for routing HTTP requests) happens only once, even if called multiple times concurrently. + routeTreesUpdated sync.Once + // RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a // handler for the path with (without) the trailing slash exists. // For example if /foo/ is requested but a route only exists for /foo, the @@ -654,6 +658,10 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) { // ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + engine.routeTreesUpdated.Do(func() { + engine.updateRouteTrees() + }) + c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req diff --git a/gin_test.go b/gin_test.go index be076537..cee1f3cc 100644 --- a/gin_test.go +++ b/gin_test.go @@ -913,3 +913,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) { assert.NotPanics(t, func() { g.ServeHTTP(resp, req) }) assert.Equal(t, http.StatusNotFound, resp.Code) } + +// Test the fix for https://github.com/gin-gonic/gin/pull/4415 +func TestLiteralColonWithRun(t *testing.T) { + SetMode(TestMode) + router := New() + + router.GET(`/test\:action`, func(c *Context) { + c.JSON(http.StatusOK, H{"path": "literal_colon"}) + }) + + router.updateRouteTrees() + + w := httptest.NewRecorder() + + req, _ := http.NewRequest(http.MethodGet, "/test:action", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "literal_colon") +} + +func TestLiteralColonWithDirectServeHTTP(t *testing.T) { + SetMode(TestMode) + router := New() + + router.GET(`/test\:action`, func(c *Context) { + c.JSON(http.StatusOK, H{"path": "literal_colon"}) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/test:action", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "literal_colon") +} + +func TestLiteralColonWithHandler(t *testing.T) { + SetMode(TestMode) + router := New() + + router.GET(`/test\:action`, func(c *Context) { + c.JSON(http.StatusOK, H{"path": "literal_colon"}) + }) + + handler := router.Handler() + + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/test:action", nil) + handler.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "literal_colon") +} + +func TestLiteralColonWithHTTPServer(t *testing.T) { + SetMode(TestMode) + router := New() + + router.GET(`/test\:action`, func(c *Context) { + c.JSON(http.StatusOK, H{"path": "literal_colon"}) + }) + + router.GET("/test/:param", func(c *Context) { + c.JSON(http.StatusOK, H{"param": c.Param("param")}) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/test:action", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "literal_colon") + + w2 := httptest.NewRecorder() + req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil) + router.ServeHTTP(w2, req2) + + assert.Equal(t, http.StatusOK, w2.Code) + assert.Contains(t, w2.Body.String(), "foo") +} + +// Test that updateRouteTrees is called only once +func TestUpdateRouteTreesCalledOnce(t *testing.T) { + SetMode(TestMode) + router := New() + + router.GET(`/test\:action`, func(c *Context) { + c.String(http.StatusOK, "ok") + }) + + for range 5 { + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/test:action", nil) + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "ok", w.Body.String()) + } +} From e88fc8927a52b74f55bec0351604a56ac0aa1c51 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 18 Nov 2025 23:05:54 +0800 Subject: [PATCH 17/28] ci(sec): schedule Trivy security scans to run daily at midnight UTC (#4439) - Change Trivy scan schedule from quarterly to daily runs at 00:00 UTC Signed-off-by: appleboy --- .github/workflows/trivy-scan.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 12830633..da31dd59 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -8,9 +8,8 @@ on: branches: - master schedule: - # Run every 3 months (quarterly) on the 1st day at 00:00 UTC - # Months: January (1), April (4), July (7), October (10) - - cron: '0 0 1 1,4,7,10 *' + # Run daily at 00:00 UTC + - cron: '0 0 * * *' workflow_dispatch: # Allow manual trigger permissions: From ecb3f7b5e2f3915bf1db240ed5eee572f8dbea36 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 23 Nov 2025 11:46:13 +0800 Subject: [PATCH 18/28] chore(deps): upgrade golang.org/x/crypto to v0.45.0 (#4449) - Update golang.org/x/crypto dependency to version 0.45.0 1. https://avd.aquasec.com/nvd/cve-2025-47914 2. https://avd.aquasec.com/nvd/cve-2025-58181 Signed-off-by: appleboy --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c756803a..628ab4c5 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/quic-go/qpack v0.5.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.44.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 1ef1ad18..90d5e526 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ 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= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 440eb14ab8ed503d4a31dfecc9946a90cd73b955 Mon Sep 17 00:00:00 2001 From: Name <1911860538@qq.com> Date: Wed, 26 Nov 2025 23:32:18 +0800 Subject: [PATCH 19/28] perf(path): replace regex with custom functions in redirectTrailingSlash (#4414) * perf: replace regex with custom functions in redirectTrailingSlash * perf: use more efficient removeRepeatedChar for path slash handling --------- Co-authored-by: 1911860538 --- gin.go | 21 ++++++++++++-------- path.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++- path_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/gin.go b/gin.go index d71086d1..16067e55 100644 --- a/gin.go +++ b/gin.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "path" - "regexp" "strings" "sync" @@ -48,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{ }, } -var ( - regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+") - regRemoveRepeatedChar = regexp.MustCompile("/{2,}") -) - // HandlerFunc defines the handler used by gin middleware as return value. type HandlerFunc func(*Context) @@ -776,8 +770,8 @@ func redirectTrailingSlash(c *Context) { req := c.Request p := req.URL.Path if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { - prefix = regSafePrefix.ReplaceAllString(prefix, "") - prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/") + prefix = sanitizePathChars(prefix) + prefix = removeRepeatedChar(prefix, '/') p = prefix + "/" + req.URL.Path } @@ -788,6 +782,17 @@ func redirectTrailingSlash(c *Context) { redirectRequest(c) } +// sanitizePathChars removes unsafe characters from path strings, +// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens. +func sanitizePathChars(s string) string { + return strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' { + return r + } + return -1 + }, s) +} + func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { req := c.Request rPath := req.URL.Path diff --git a/path.go b/path.go index 82438c13..3b67caa9 100644 --- a/path.go +++ b/path.go @@ -5,6 +5,8 @@ package gin +const stackBufSize = 128 + // cleanPath is the URL version of path.Clean, it returns a canonical URL path // for p, eliminating . and .. elements. // @@ -19,7 +21,6 @@ package gin // // If the result of this process is an empty string, "/" is returned. func cleanPath(p string) string { - const stackBufSize = 128 // Turn empty string into "/" if p == "" { return "/" @@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) { } b[w] = c } + +// removeRepeatedChar removes multiple consecutive 'char's from a string. +// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/" +func removeRepeatedChar(s string, char byte) string { + // Check if there are any consecutive chars + hasRepeatedChar := false + for i := 1; i < len(s); i++ { + if s[i] == char && s[i-1] == char { + hasRepeatedChar = true + break + } + } + if !hasRepeatedChar { + return s + } + + // Reasonably sized buffer on stack to avoid allocations in the common case. + buf := make([]byte, 0, stackBufSize) + + // Invariants: + // reading from s; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + r := 0 + w := 0 + + for n := len(s); r < n; { + if s[r] == char { + // Write the first char + bufApp(&buf, s, w, char) + w++ + r++ + + // Skip all consecutive chars + for r < n && s[r] == char { + r++ + } + } else { + // Copy non-char character + bufApp(&buf, s, w, s[r]) + w++ + r++ + } + } + + // If the original string was not modified (or only shortened at the end), + // return the respective substring of the original string. + // Otherwise, return a new string from the buffer. + if len(buf) == 0 { + return s[:w] + } + return string(buf[:w]) +} diff --git a/path_test.go b/path_test.go index 7d86086f..eba1be08 100644 --- a/path_test.go +++ b/path_test.go @@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) { } } } + +func TestRemoveRepeatedChar(t *testing.T) { + testCases := []struct { + name string + str string + char byte + want string + }{ + { + name: "empty", + str: "", + char: 'a', + want: "", + }, + { + name: "noSlash", + str: "abc", + char: ',', + want: "abc", + }, + { + name: "withSlash", + str: "/a/b/c/", + char: '/', + want: "/a/b/c/", + }, + { + name: "withRepeatedSlashes", + str: "/a//b///c////", + char: '/', + want: "/a/b/c/", + }, + { + name: "threeSlashes", + str: "///", + char: '/', + want: "/", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res := removeRepeatedChar(tc.str, tc.char) + assert.Equal(t, tc.want, res) + }) + } +} From 52ecf029bd2e9b4d2652f96dd2b753f8bc6b6e95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:33:08 +0800 Subject: [PATCH 20/28] chore(deps): bump actions/checkout from 5 to 6 in the actions group (#4446) Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bo-Yi Wu --- .github/workflows/codeql.yml | 2 +- .github/workflows/gin.yml | 4 ++-- .github/workflows/goreleaser.yml | 2 +- .github/workflows/trivy-scan.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9ec3700e..f287c265 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index 8bca364d..4e3b8753 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go @@ -61,7 +61,7 @@ jobs: cache: false - name: Checkout Code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.ref }} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 37dfb5bb..0098b952 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index da31dd59..b86aed7f 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 From 771dcc6476d7bc6abb9ec0235ecefa4d38fe6fb0 Mon Sep 17 00:00:00 2001 From: Aeddis Desauw <89919264+ldesauw@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:55:34 +0100 Subject: [PATCH 21/28] feat(gin): add option to use escaped path (#4420) Co-authored-by: Bo-Yi Wu --- gin.go | 16 ++++++++++++++-- gin_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/gin.go b/gin.go index 16067e55..2e033bf3 100644 --- a/gin.go +++ b/gin.go @@ -135,10 +135,16 @@ type Engine struct { AppEngine bool // UseRawPath if enabled, the url.RawPath will be used to find parameters. + // The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL) + // Only use RawPath if you know what you are doing. UseRawPath bool + // UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters + // It overrides UseRawPath + UseEscapedPath bool + // UnescapePathValues if true, the path value will be unescaped. - // If UseRawPath is false (by default), the UnescapePathValues effectively is true, + // If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true, // as url.Path gonna be used, which is already unescaped. UnescapePathValues bool @@ -191,6 +197,7 @@ var _ IRouter = (*Engine)(nil) // - HandleMethodNotAllowed: false // - ForwardedByClientIP: true // - UseRawPath: false +// - UseEscapedPath: false // - UnescapePathValues: true func New(opts ...OptionFunc) *Engine { debugPrintWARNINGNew() @@ -208,6 +215,7 @@ func New(opts ...OptionFunc) *Engine { RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, TrustedPlatform: defaultPlatform, UseRawPath: false, + UseEscapedPath: false, RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, @@ -683,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path unescape := false - if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { + + if engine.UseEscapedPath { + rPath = c.Request.URL.EscapedPath() + unescape = engine.UnescapePathValues + } else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { rPath = c.Request.URL.RawPath unescape = engine.UnescapePathValues } diff --git a/gin_test.go b/gin_test.go index cee1f3cc..21bf71d8 100644 --- a/gin_test.go +++ b/gin_test.go @@ -720,6 +720,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) { assert.Equal(t, int64(1), handlerCounterV2) } +func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) { + r := New() + r.UseEscapedPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + // Path is Escaped, the %25 is not interpreted as % + assert.Equal(t, "foo%252Fbar", c.Param("path")) + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) +} + +func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) { + r := New() + r.UseRawPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + // Path is used, the %25 is interpreted as % + assert.Equal(t, "foo%2Fbar", c.Param("path")) + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) +} + +func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) { + r := New() + r.UseEscapedPath = true + r.UseRawPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + assert.Equal(t, "foo%25bar", c.Param("path")) + c.Status(http.StatusOK) + }) + + assert.NotPanics(t, func() { + w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar") + assert.Equal(t, 200, w.Code) + }) +} + func TestPrepareTrustedCIRDsWith(t *testing.T) { r := New() From c358d5656d0feb8b310d4ec379bccde46ccc8cc7 Mon Sep 17 00:00:00 2001 From: Milad Date: Thu, 27 Nov 2025 18:31:57 +0330 Subject: [PATCH 22/28] test(gin): Add comprehensive test coverage for ginS package (#4442) * test(ginS): add comprehensive test coverage for ginS package Improve test coverage for ginS package by adding 18 test functions covering HTTP methods, routing, middleware, static files, and templates. * use http.Method* constants instead of raw strings in gins_test.go * copyright updated in gins_test.go --------- Co-authored-by: Bo-Yi Wu --- ginS/gins_test.go | 246 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 ginS/gins_test.go diff --git a/ginS/gins_test.go b/ginS/gins_test.go new file mode 100644 index 00000000..ffde85d2 --- /dev/null +++ b/ginS/gins_test.go @@ -0,0 +1,246 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package ginS + +import ( + "html/template" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestGET(t *testing.T) { + GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "test") + }) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "test", w.Body.String()) +} + +func TestPOST(t *testing.T) { + POST("/post", func(c *gin.Context) { + c.String(http.StatusCreated, "created") + }) + + req := httptest.NewRequest(http.MethodPost, "/post", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "created", w.Body.String()) +} + +func TestPUT(t *testing.T) { + PUT("/put", func(c *gin.Context) { + c.String(http.StatusOK, "updated") + }) + + req := httptest.NewRequest(http.MethodPut, "/put", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "updated", w.Body.String()) +} + +func TestDELETE(t *testing.T) { + DELETE("/delete", func(c *gin.Context) { + c.String(http.StatusOK, "deleted") + }) + + req := httptest.NewRequest(http.MethodDelete, "/delete", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "deleted", w.Body.String()) +} + +func TestPATCH(t *testing.T) { + PATCH("/patch", func(c *gin.Context) { + c.String(http.StatusOK, "patched") + }) + + req := httptest.NewRequest(http.MethodPatch, "/patch", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "patched", w.Body.String()) +} + +func TestOPTIONS(t *testing.T) { + OPTIONS("/options", func(c *gin.Context) { + c.String(http.StatusOK, "options") + }) + + req := httptest.NewRequest(http.MethodOptions, "/options", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "options", w.Body.String()) +} + +func TestHEAD(t *testing.T) { + HEAD("/head", func(c *gin.Context) { + c.String(http.StatusOK, "head") + }) + + req := httptest.NewRequest(http.MethodHead, "/head", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestAny(t *testing.T) { + Any("/any", func(c *gin.Context) { + c.String(http.StatusOK, "any") + }) + + req := httptest.NewRequest(http.MethodGet, "/any", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "any", w.Body.String()) +} + +func TestHandle(t *testing.T) { + Handle(http.MethodGet, "/handle", func(c *gin.Context) { + c.String(http.StatusOK, "handle") + }) + + req := httptest.NewRequest(http.MethodGet, "/handle", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "handle", w.Body.String()) +} + +func TestGroup(t *testing.T) { + group := Group("/group") + group.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "group test") + }) + + req := httptest.NewRequest(http.MethodGet, "/group/test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "group test", w.Body.String()) +} + +func TestUse(t *testing.T) { + var middlewareExecuted bool + Use(func(c *gin.Context) { + middlewareExecuted = true + c.Next() + }) + + GET("/middleware-test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/middleware-test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.True(t, middlewareExecuted) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestNoRoute(t *testing.T) { + NoRoute(func(c *gin.Context) { + c.String(http.StatusNotFound, "custom 404") + }) + + req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Equal(t, "custom 404", w.Body.String()) +} + +func TestNoMethod(t *testing.T) { + NoMethod(func(c *gin.Context) { + c.String(http.StatusMethodNotAllowed, "method not allowed") + }) + + // This just verifies that NoMethod is callable + // Testing the actual behavior would require a separate engine instance + assert.NotNil(t, engine()) +} + +func TestRoutes(t *testing.T) { + GET("/routes-test", func(c *gin.Context) {}) + + routes := Routes() + assert.NotEmpty(t, routes) + + found := false + for _, route := range routes { + if route.Path == "/routes-test" && route.Method == http.MethodGet { + found = true + break + } + } + assert.True(t, found) +} + +func TestSetHTMLTemplate(t *testing.T) { + tmpl := template.Must(template.New("test").Parse("Hello {{.}}")) + SetHTMLTemplate(tmpl) + + // Verify engine has template set + assert.NotNil(t, engine()) +} + +func TestStaticFile(t *testing.T) { + StaticFile("/static-file", "../testdata/test_file.txt") + + req := httptest.NewRequest(http.MethodGet, "/static-file", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestStatic(t *testing.T) { + Static("/static-dir", "../testdata") + + req := httptest.NewRequest(http.MethodGet, "/static-dir/test_file.txt", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestStaticFS(t *testing.T) { + fs := http.Dir("../testdata") + StaticFS("/static-fs", fs) + + req := httptest.NewRequest(http.MethodGet, "/static-fs/test_file.txt", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} From 63dd3e60cab89c27fb66bce1423bd268d52abad1 Mon Sep 17 00:00:00 2001 From: Yilong Li Date: Thu, 27 Nov 2025 23:20:52 +0800 Subject: [PATCH 23/28] fix(recover): suppress http.ErrAbortHandler in recover (#4336) Co-authored-by: Bo-Yi Wu --- recovery.go | 3 +++ recovery_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/recovery.go b/recovery.go index fdd463f3..e79e118a 100644 --- a/recovery.go +++ b/recovery.go @@ -68,6 +68,9 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { } } } + if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) { + brokenPipe = true + } if logger != nil { const stackSkip = 3 if brokenPipe { diff --git a/recovery_test.go b/recovery_test.go index 8a9e3475..073f4858 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -142,6 +142,30 @@ func TestPanicWithBrokenPipe(t *testing.T) { } } +// TestPanicWithAbortHandler asserts that recovery handles http.ErrAbortHandler as broken pipe +func TestPanicWithAbortHandler(t *testing.T) { + const expectCode = 204 + + var buf strings.Builder + router := New() + router.Use(RecoveryWithWriter(&buf)) + router.GET("/recovery", func(c *Context) { + // Start writing response + c.Header("X-Test", "Value") + c.Status(expectCode) + + // Panic with ErrAbortHandler which should be treated as broken pipe + panic(http.ErrAbortHandler) + }) + // RUN + w := PerformRequest(router, http.MethodGet, "/recovery") + // TEST + assert.Equal(t, expectCode, w.Code) + out := buf.String() + assert.Contains(t, out, "net/http: abort Handler") + assert.NotContains(t, out, "panic recovered") +} + func TestCustomRecoveryWithWriter(t *testing.T) { errBuffer := new(strings.Builder) buffer := new(strings.Builder) From af6e8b70b8261bb0c99ad094fe552ab92991620a Mon Sep 17 00:00:00 2001 From: appleboy Date: Sun, 30 Nov 2025 11:52:25 +0800 Subject: [PATCH 24/28] chore(deps): upgrade quic-go to v0.57.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix CVE-2025-59530 vulnerability (quic-go Crash Due to Premature HANDSHAKE_DONE Frame) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 628ab4c5..58ec6fc9 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,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.56.0 + github.com/quic-go/quic-go v0.57.1 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 golang.org/x/net v0.47.0 @@ -32,7 +32,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.45.0 // indirect diff --git a/go.sum b/go.sum index 90d5e526..bcdb4493 100644 --- a/go.sum +++ b/go.sum @@ -49,10 +49,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= -github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 583db590ec2a488ebcf7f8dc6232d11c7db62eac Mon Sep 17 00:00:00 2001 From: Milad Date: Sun, 30 Nov 2025 10:55:46 +0330 Subject: [PATCH 25/28] test(bytesconv): add tests for empty/nil cases (#4454) Co-authored-by: Bo-Yi Wu --- internal/bytesconv/bytesconv_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/bytesconv/bytesconv_test.go b/internal/bytesconv/bytesconv_test.go index 4972ae70..60e28fb4 100644 --- a/internal/bytesconv/bytesconv_test.go +++ b/internal/bytesconv/bytesconv_test.go @@ -41,6 +41,15 @@ func TestBytesToString(t *testing.T) { } } +func TestBytesToStringEmpty(t *testing.T) { + if got := BytesToString([]byte{}); got != "" { + t.Fatalf("BytesToString([]byte{}) = %q; want empty string", got) + } + if got := BytesToString(nil); got != "" { + t.Fatalf("BytesToString(nil) = %q; want empty string", got) + } +} + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index @@ -78,6 +87,16 @@ func TestStringToBytes(t *testing.T) { } } +func TestStringToBytesEmpty(t *testing.T) { + b := StringToBytes("") + if len(b) != 0 { + t.Fatalf(`StringToBytes("") length = %d; want 0`, len(b)) + } + if !bytes.Equal(b, []byte("")) { + t.Fatalf(`StringToBytes("") = %v; want []byte("")`, b) + } +} + // go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true func BenchmarkBytesConvBytesToStrRaw(b *testing.B) { From f416d1e594a027063e73f66ac873a82113036fd8 Mon Sep 17 00:00:00 2001 From: Wayne Aki <111057868+Planckbaka@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:38:07 +0800 Subject: [PATCH 26/28] test(gin): resolve race conditions in integration tests (#4453) - Implement TestRebuild404Handlers to verify 404 handler chain rebuilding when global middleware is added via Use() - Add waitForServerReady helper with exponential backoff to replace unreliable time.Sleep() calls in integration tests - Fix race conditions in TestRunEmpty, TestRunEmptyWithEnv, and TestRunWithPort by using proper server readiness checks - All tests now pass consistently with -race flag This addresses the empty test function and eliminates flaky test failures caused by insufficient wait times for server startup. Co-authored-by: Bo-Yi Wu --- gin_integration_test.go | 21 ++++++++++++--------- gin_test.go | 23 +++++++++++++++++++++++ test_helpers.go | 31 ++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/gin_integration_test.go b/gin_integration_test.go index e040993a..3ea5fe2f 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -70,9 +70,10 @@ func TestRunEmpty(t *testing.T) { router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) assert.NoError(t, router.Run()) }() - // have to wait for the goroutine to start and run the server - // otherwise the main thread will complete - time.Sleep(5 * time.Millisecond) + + // Wait for server to be ready with exponential backoff + err := waitForServerReady("http://localhost:8080/example", 10) + require.NoError(t, err, "server should start successfully") require.Error(t, router.Run(":8080")) testRequest(t, "http://localhost:8080/example") @@ -213,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) { router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) assert.NoError(t, router.Run()) }() - // have to wait for the goroutine to start and run the server - // otherwise the main thread will complete - time.Sleep(5 * time.Millisecond) + + // Wait for server to be ready with exponential backoff + err := waitForServerReady("http://localhost:3123/example", 10) + require.NoError(t, err, "server should start successfully") require.Error(t, router.Run(":3123")) testRequest(t, "http://localhost:3123/example") @@ -234,9 +236,10 @@ func TestRunWithPort(t *testing.T) { router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) assert.NoError(t, router.Run(":5150")) }() - // have to wait for the goroutine to start and run the server - // otherwise the main thread will complete - time.Sleep(5 * time.Millisecond) + + // Wait for server to be ready with exponential backoff + err := waitForServerReady("http://localhost:5150/example", 10) + require.NoError(t, err, "server should start successfully") require.Error(t, router.Run(":5150")) testRequest(t, "http://localhost:5150/example") diff --git a/gin_test.go b/gin_test.go index 21bf71d8..81343d88 100644 --- a/gin_test.go +++ b/gin_test.go @@ -545,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) { } func TestRebuild404Handlers(t *testing.T) { + var middleware0 HandlerFunc = func(c *Context) {} + var middleware1 HandlerFunc = func(c *Context) {} + + router := New() + + // Initially, allNoRoute should be nil + assert.Nil(t, router.allNoRoute) + + // Set NoRoute handlers + router.NoRoute(middleware0) + assert.Len(t, router.allNoRoute, 1) + assert.Len(t, router.noRoute, 1) + compareFunc(t, router.allNoRoute[0], middleware0) + + // Add Use middleware should trigger rebuild404Handlers + router.Use(middleware1) + assert.Len(t, router.allNoRoute, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noRoute, 1) + + // Global middleware should come first + compareFunc(t, router.allNoRoute[0], middleware1) + compareFunc(t, router.allNoRoute[1], middleware0) } func TestNoMethodWithGlobalHandlers(t *testing.T) { diff --git a/test_helpers.go b/test_helpers.go index a1a7c562..20d20032 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -4,7 +4,11 @@ package gin -import "net/http" +import ( + "fmt" + "net/http" + "time" +) // CreateTestContext returns a fresh Engine and a Context associated with it. // This is useful for tests that need to set up a new Gin engine instance @@ -29,3 +33,28 @@ func CreateTestContextOnly(w http.ResponseWriter, r *Engine) (c *Context) { c.writermem.reset(w) return } + +// waitForServerReady waits for a server to be ready by making HTTP requests +// with exponential backoff. This is more reliable than time.Sleep() for testing. +func waitForServerReady(url string, maxAttempts int) error { + client := &http.Client{ + Timeout: 100 * time.Millisecond, + } + + for i := 0; i < maxAttempts; i++ { + resp, err := client.Get(url) + if err == nil { + resp.Body.Close() + return nil + } + + // Exponential backoff: 10ms, 20ms, 40ms, 80ms, 160ms... + backoff := time.Duration(10*(1< 500*time.Millisecond { + backoff = 500 * time.Millisecond + } + time.Sleep(backoff) + } + + return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts) +} From fad706f1216e6d12bdd51d28d5a40ec27e6c6453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:09:41 +0800 Subject: [PATCH 27/28] chore(deps): bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0 (#4458) Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0. - [Release notes](https://github.com/goccy/go-yaml/releases) - [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/goccy/go-yaml/compare/v1.18.0...v1.19.0) --- updated-dependencies: - dependency-name: github.com/goccy/go-yaml dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 58ec6fc9..3a2b2bf2 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.18.0 + github.com/goccy/go-yaml v1.19.0 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 diff --git a/go.sum b/go.sum index bcdb4493..a487aaaf 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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= From b917b14ff9d189f16a7492be79d123a47806ee19 Mon Sep 17 00:00:00 2001 From: guonaihong Date: Wed, 3 Dec 2025 19:18:10 +0800 Subject: [PATCH 28/28] fix(binding): empty value error (#2169) * fix empty value error Here is the code that can report an error ```go package main import ( "fmt" "github.com/gin-gonic/gin" "io" "net/http" "os" "time" ) type header struct { Duration time.Duration `header:"duration"` CreateTime time.Time `header:"createTime" time_format:"unix"` } func needFix1() { g := gin.Default() g.GET("/", func(c *gin.Context) { h := header{} err := c.ShouldBindHeader(&h) if err != nil { c.JSON(500, fmt.Sprintf("fail:%s\n", err)) return } c.JSON(200, h) }) g.Run(":8081") } func needFix2() { g := gin.Default() g.GET("/", func(c *gin.Context) { h := header{} err := c.ShouldBindHeader(&h) if err != nil { c.JSON(500, fmt.Sprintf("fail:%s\n", err)) return } c.JSON(200, h) }) g.Run(":8082") } func sendNeedFix1() { // send to needFix1 sendBadData("http://127.0.0.1:8081", "duration") } func sendNeedFix2() { // send to needFix2 sendBadData("http://127.0.0.1:8082", "createTime") } func sendBadData(url, key string) { req, err := http.NewRequest("GET", "http://127.0.0.1:8081", nil) if err != nil { fmt.Printf("err:%s\n", err) return } // Only the key and no value can cause an error req.Header.Add(key, "") rsp, err := http.DefaultClient.Do(req) if err != nil { return } io.Copy(os.Stdout, rsp.Body) rsp.Body.Close() } func main() { go needFix1() go needFix2() time.Sleep(time.Second / 1000 * 200) // 200ms sendNeedFix1() sendNeedFix2() } ``` * modify code * add comment * test(binding): use 'any' alias and require.NoError in form mapping tests - Replace 'interface{}' with 'any' alias in bindTestData struct - Change assert.NoError to require.NoError in TestMappingTimeUnixNano and TestMappingTimeDuration to fail fast on mapping errors --------- Co-authored-by: Bo-Yi Wu --- binding/form_mapping.go | 19 +++++++++++++----- binding/form_mapping_test.go | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 1244b522..e76e7510 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -300,6 +300,11 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { + // If it is a string type, no spaces are removed, and the user data is not modified here + if value.Kind() != reflect.String { + val = strings.TrimSpace(val) + } + switch value.Kind() { case reflect.Int: return setIntField(val, 0, value) @@ -404,6 +409,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val timeFormat = time.RFC3339 } + if val == "" { + value.Set(reflect.ValueOf(time.Time{})) + return nil + } + switch tf := strings.ToLower(timeFormat); tf { case "unix", "unixmilli", "unixmicro", "unixnano": tv, err := strconv.ParseInt(val, 10, 64) @@ -427,11 +437,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val return nil } - if val == "" { - value.Set(reflect.ValueOf(time.Time{})) - return nil - } - l := time.Local if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { l = time.UTC @@ -475,6 +480,10 @@ func setSlice(vals []string, value reflect.Value, field reflect.StructField) err } func setTimeDuration(val string, value reflect.Value) error { + if val == "" { + val = "0" + } + d, err := time.ParseDuration(val) if err != nil { return err diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 006eddf1..e007573c 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -226,7 +226,35 @@ func TestMappingTime(t *testing.T) { require.Error(t, err) } +type bindTestData struct { + need any + got any + in map[string][]string +} + +func TestMappingTimeUnixNano(t *testing.T) { + type needFixUnixNanoEmpty struct { + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + } + + // ok + tests := []bindTestData{ + {need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{" "}}}, + {need: &needFixUnixNanoEmpty{}, got: &needFixUnixNanoEmpty{}, in: formSource{"createTime": []string{}}}, + } + + for _, v := range tests { + err := mapForm(v.got, v.in) + require.NoError(t, err) + assert.Equal(t, v.need, v.got) + } +} + func TestMappingTimeDuration(t *testing.T) { + type needFixDurationEmpty struct { + Duration time.Duration `form:"duration"` + } + var s struct { D time.Duration } @@ -236,6 +264,17 @@ func TestMappingTimeDuration(t *testing.T) { require.NoError(t, err) assert.Equal(t, 5*time.Second, s.D) + // ok + tests := []bindTestData{ + {need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{" "}}}, + {need: &needFixDurationEmpty{}, got: &needFixDurationEmpty{}, in: formSource{"duration": []string{}}}, + } + + for _, v := range tests { + err := mapForm(v.got, v.in) + require.NoError(t, err) + assert.Equal(t, v.need, v.got) + } // error err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") require.Error(t, err)