Merge branch 'master' into fix-binding-custom-unmarhsal-4296

This commit is contained in:
Bo-Yi Wu 2025-11-30 15:41:08 +08:00 committed by GitHub
commit ef09093976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 775 additions and 97 deletions

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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:
@ -23,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@ -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

View File

@ -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")
}

View File

@ -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")

View File

@ -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)
}
@ -830,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 {

View File

@ -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)

75
gin.go
View File

@ -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
}
@ -630,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
@ -663,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
}
@ -750,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
}
@ -762,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

246
ginS/gins_test.go Normal file
View File

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

View File

@ -16,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")
}

View File

@ -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())
}
}

16
go.mod
View File

@ -12,10 +12,10 @@ 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.57.1
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
)
@ -28,17 +28,15 @@ 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
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
)

42
go.sum
View File

@ -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=
@ -44,10 +49,12 @@ 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=
@ -67,25 +74,22 @@ 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=

View File

@ -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) {

55
path.go
View File

@ -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])
}

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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<<uint(i))) * time.Millisecond
if backoff > 500*time.Millisecond {
backoff = 500 * time.Millisecond
}
time.Sleep(backoff)
}
return fmt.Errorf("server at %s did not become ready after %d attempts", url, maxAttempts)
}

13
tree.go
View File

@ -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

View File

@ -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")
}
}