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 f61c6486..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
@@ -24,9 +24,9 @@ jobs:
with:
go-version: "^1"
- name: Setup golangci-lint
- uses: golangci/golangci-lint-action@v8
+ uses: golangci/golangci-lint-action@v9
with:
- version: v2.1.6
+ version: v2.6
args: --verbose
test:
needs: lint
@@ -61,7 +61,7 @@ jobs:
cache: false
- name: Checkout Code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
@@ -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/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
new file mode 100644
index 00000000..b86aed7f
--- /dev/null
+++ b/.github/workflows/trivy-scan.yml
@@ -0,0 +1,56 @@
+name: Trivy Security Scan
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+ schedule:
+ # Run daily at 00:00 UTC
+ - cron: '0 0 * * *'
+ workflow_dispatch: # Allow manual trigger
+
+permissions:
+ contents: read
+ security-events: write # Required for uploading SARIF results
+
+jobs:
+ trivy-scan:
+ name: Trivy Security Scan
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Run Trivy vulnerability scanner (source code)
+ uses: aquasecurity/trivy-action@0.33.1
+ with:
+ scan-type: 'fs'
+ scan-ref: '.'
+ scanners: 'vuln,secret,misconfig'
+ format: 'sarif'
+ output: 'trivy-results.sarif'
+ severity: 'CRITICAL,HIGH,MEDIUM'
+ ignore-unfixed: true
+
+ - name: Upload Trivy results to GitHub Security tab
+ uses: github/codeql-action/upload-sarif@v4
+ if: always()
+ with:
+ sarif_file: 'trivy-results.sarif'
+
+ - name: Run Trivy scanner (table output for logs)
+ uses: aquasecurity/trivy-action@0.33.1
+ if: always()
+ with:
+ scan-type: 'fs'
+ scan-ref: '.'
+ scanners: 'vuln,secret,misconfig'
+ format: 'table'
+ severity: 'CRITICAL,HIGH,MEDIUM'
+ ignore-unfixed: true
+ exit-code: '1'
diff --git a/.golangci.yml b/.golangci.yml
index d8887062..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
@@ -68,7 +61,6 @@ linters:
- examples$
formatters:
enable:
- - gci
- gofmt
- gofumpt
- goimports
@@ -80,7 +72,4 @@ formatters:
exclusions:
generated: lax
paths:
- - third_party$
- - builtin$
- - examples$
- gin.go
diff --git a/README.md b/README.md
index 629cb98d..1b9ab808 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
[](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
+[](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
[](https://codecov.io/gh/gin-gonic/gin)
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
[](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
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.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_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/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)
diff --git a/context.go b/context.go
index e64c7953..112f0ee0 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.
@@ -54,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 {
@@ -185,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)
}
@@ -829,41 +838,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 {
@@ -1280,14 +1319,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 +1353,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..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)
@@ -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)
diff --git a/gin.go b/gin.go
index 1965a429..2e033bf3 100644
--- a/gin.go
+++ b/gin.go
@@ -11,7 +11,6 @@ import (
"net/http"
"os"
"path"
- "regexp"
"strings"
"sync"
@@ -23,10 +22,12 @@ import (
"golang.org/x/net/http2/h2c"
)
-const defaultMultipartMemory = 32 << 20 // 32 MB
-const escapedColon = "\\:"
-const colon = ":"
-const backslash = "\\"
+const (
+ defaultMultipartMemory = 32 << 20 // 32 MB
+ escapedColon = "\\:"
+ colon = ":"
+ backslash = "\\"
+)
var (
default404Body = []byte("404 page not found")
@@ -46,9 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
},
}
-var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
-var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
-
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
@@ -94,6 +92,10 @@ const (
type Engine struct {
RouterGroup
+ // routeTreesUpdated ensures that the initialization or update of the route trees
+ // (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
+ routeTreesUpdated sync.Once
+
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
@@ -133,10 +135,16 @@ type Engine struct {
AppEngine bool
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
+ // The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL)
+ // Only use RawPath if you know what you are doing.
UseRawPath bool
+ // UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters
+ // It overrides UseRawPath
+ UseEscapedPath bool
+
// UnescapePathValues if true, the path value will be unescaped.
- // If UseRawPath is false (by default), the UnescapePathValues effectively is true,
+ // If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
@@ -189,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
+// - UseEscapedPath: false
// - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
@@ -206,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
+ UseEscapedPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
@@ -537,7 +547,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
- err = http.ListenAndServe(address, engine.Handler())
+ server := &http.Server{ // #nosec G112
+ Addr: address,
+ Handler: engine.Handler(),
+ }
+ err = server.ListenAndServe()
return
}
@@ -553,7 +567,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
- err = http.ListenAndServeTLS(addr, certFile, keyFile, engine.Handler())
+ server := &http.Server{ // #nosec G112
+ Addr: addr,
+ Handler: engine.Handler(),
+ }
+ err = server.ListenAndServeTLS(certFile, keyFile)
return
}
@@ -576,7 +594,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
defer listener.Close()
defer os.Remove(file)
- err = http.Serve(listener, engine.Handler())
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+ err = server.Serve(listener)
return
}
@@ -593,6 +614,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
}
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
+ defer f.Close()
listener, err := net.FileListener(f)
if err != nil {
return
@@ -629,12 +651,19 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
- err = http.Serve(listener, engine.Handler())
+ server := &http.Server{ // #nosec G112
+ Handler: engine.Handler(),
+ }
+ err = server.Serve(listener)
return
}
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ engine.routeTreesUpdated.Do(func() {
+ engine.updateRouteTrees()
+ })
+
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
@@ -662,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
- if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
+
+ if engine.UseEscapedPath {
+ rPath = c.Request.URL.EscapedPath()
+ unescape = engine.UnescapePathValues
+ } else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
@@ -749,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
req := c.Request
p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
- prefix = regSafePrefix.ReplaceAllString(prefix, "")
- prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
+ prefix = sanitizePathChars(prefix)
+ prefix = removeRepeatedChar(prefix, '/')
p = prefix + "/" + req.URL.Path
}
@@ -761,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
redirectRequest(c)
}
+// sanitizePathChars removes unsafe characters from path strings,
+// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
+func sanitizePathChars(s string) string {
+ return strings.Map(func(r rune) rune {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
+ return r
+ }
+ return -1
+ }, s)
+}
+
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
rPath := req.URL.Path
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)
+}
diff --git a/gin_integration_test.go b/gin_integration_test.go
index c032d837..3ea5fe2f 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"runtime"
+ "strings"
"sync"
"testing"
"time"
@@ -69,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
- // have to wait for the goroutine to start and run the server
- // otherwise the main thread will complete
- time.Sleep(5 * time.Millisecond)
+
+ // Wait for server to be ready with exponential backoff
+ err := waitForServerReady("http://localhost:8080/example", 10)
+ require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example")
@@ -212,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run())
}()
- // have to wait for the goroutine to start and run the server
- // otherwise the main thread will complete
- time.Sleep(5 * time.Millisecond)
+
+ // Wait for server to be ready with exponential backoff
+ err := waitForServerReady("http://localhost:3123/example", 10)
+ require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":3123"))
testRequest(t, "http://localhost:3123/example")
@@ -233,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.Run(":5150"))
}()
- // have to wait for the goroutine to start and run the server
- // otherwise the main thread will complete
- time.Sleep(5 * time.Millisecond)
+
+ // Wait for server to be ready with exponential backoff
+ err := waitForServerReady("http://localhost:5150/example", 10)
+ require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":5150"))
testRequest(t, "http://localhost:5150/example")
@@ -261,10 +265,11 @@ func TestUnixSocket(t *testing.T) {
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
- var response string
+ var responseBuilder strings.Builder
for scanner.Scan() {
- response += scanner.Text()
+ responseBuilder.WriteString(scanner.Text())
}
+ response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@@ -322,10 +327,11 @@ func TestFileDescriptor(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
- var response string
+ var responseBuilder strings.Builder
for scanner.Scan() {
- response += scanner.Text()
+ responseBuilder.WriteString(scanner.Text())
}
+ response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
@@ -354,10 +360,11 @@ func TestListener(t *testing.T) {
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
- var response string
+ var responseBuilder strings.Builder
for scanner.Scan() {
- response += scanner.Text()
+ responseBuilder.WriteString(scanner.Text())
}
+ response := responseBuilder.String()
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
diff --git a/gin_test.go b/gin_test.go
index be076537..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) {
@@ -720,6 +743,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()
@@ -913,3 +985,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())
+ }
+}
diff --git a/go.mod b/go.mod
index 961916f0..3a2b2bf2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,41 +3,40 @@ 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
- 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
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.57.1
github.com/stretchr/testify v1.11.1
- github.com/ugorji/go/codec v1.3.0
- golang.org/x/net v0.46.0
+ github.com/ugorji/go/codec v1.3.1
+ golang.org/x/net v0.47.0
google.golang.org/protobuf v1.36.10
)
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
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
- 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.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
+ 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 2dfb4d75..a487aaaf 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,12 @@
-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/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=
@@ -21,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=
@@ -30,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=
@@ -42,46 +49,47 @@ 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.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
-github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
+github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
+github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
+github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
+github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
-github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+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=
-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/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=
-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/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=
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=
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) {
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 2269b78e..eba1be08 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,12 +134,59 @@ func TestPathCleanLong(t *testing.T) {
func BenchmarkPathCleanLong(b *testing.B) {
cleanTests := genLongPaths()
- b.ResetTimer()
+
b.ReportAllocs()
- for i := 0; i < b.N; i++ {
+ for b.Loop() {
for _, test := range cleanTests {
cleanPath(test.path)
}
}
}
+
+func TestRemoveRepeatedChar(t *testing.T) {
+ testCases := []struct {
+ name string
+ str string
+ char byte
+ want string
+ }{
+ {
+ name: "empty",
+ str: "",
+ char: 'a',
+ want: "",
+ },
+ {
+ name: "noSlash",
+ str: "abc",
+ char: ',',
+ want: "abc",
+ },
+ {
+ name: "withSlash",
+ str: "/a/b/c/",
+ char: '/',
+ want: "/a/b/c/",
+ },
+ {
+ name: "withRepeatedSlashes",
+ str: "/a//b///c////",
+ char: '/',
+ want: "/a/b/c/",
+ },
+ {
+ name: "threeSlashes",
+ str: "///",
+ char: '/',
+ want: "/",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ res := removeRepeatedChar(tc.str, tc.char)
+ assert.Equal(t, tc.want, res)
+ })
+ }
+}
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)
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)
+}
diff --git a/tree.go b/tree.go
index 78479b6f..eff07734 100644
--- a/tree.go
+++ b/tree.go
@@ -5,7 +5,7 @@
package gin
import (
- "bytes"
+ "math"
"net/url"
"strings"
"unicode"
@@ -14,12 +14,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
@@ -84,17 +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 {
- var n uint16
- s := bytesconv.StringToBytes(path)
- n += uint16(bytes.Count(s, strColon))
- n += uint16(bytes.Count(s, strStar))
- return n
+ colons := strings.Count(path, ":")
+ stars := strings.Count(path, "*")
+ return safeUint16(colons + stars)
}
func countSections(path string) uint16 {
- s := bytesconv.StringToBytes(path)
- return uint16(bytes.Count(s, strSlash))
+ return safeUint16(strings.Count(path, "/"))
}
type nodeType uint8
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")
}
}