mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-06 03:08:11 +08:00
Merge branch 'master' into fix-router-static-fs
This commit is contained in:
commit
c19d29e913
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
6
.github/workflows/gin.yml
vendored
6
.github/workflows/gin.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
@ -61,11 +61,11 @@ jobs:
|
|||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ matrix.go-build }}
|
${{ matrix.go-build }}
|
||||||
|
|||||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
|
|||||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
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() {
|
switch value.Kind() {
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
return setIntField(val, 0, value)
|
return setIntField(val, 0, value)
|
||||||
@ -404,6 +409,11 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
timeFormat = time.RFC3339
|
timeFormat = time.RFC3339
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
value.Set(reflect.ValueOf(time.Time{}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
switch tf := strings.ToLower(timeFormat); tf {
|
switch tf := strings.ToLower(timeFormat); tf {
|
||||||
case "unix", "unixmilli", "unixmicro", "unixnano":
|
case "unix", "unixmilli", "unixmicro", "unixnano":
|
||||||
tv, err := strconv.ParseInt(val, 10, 64)
|
tv, err := strconv.ParseInt(val, 10, 64)
|
||||||
@ -427,11 +437,6 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
value.Set(reflect.ValueOf(time.Time{}))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l := time.Local
|
l := time.Local
|
||||||
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
|
||||||
l = time.UTC
|
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 {
|
func setTimeDuration(val string, value reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "0"
|
||||||
|
}
|
||||||
|
|
||||||
d, err := time.ParseDuration(val)
|
d, err := time.ParseDuration(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -226,7 +226,35 @@ func TestMappingTime(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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) {
|
func TestMappingTimeDuration(t *testing.T) {
|
||||||
|
type needFixDurationEmpty struct {
|
||||||
|
Duration time.Duration `form:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
var s struct {
|
var s struct {
|
||||||
D time.Duration
|
D time.Duration
|
||||||
}
|
}
|
||||||
@ -236,6 +264,17 @@ func TestMappingTimeDuration(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 5*time.Second, s.D)
|
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
|
// error
|
||||||
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|||||||
11
context.go
11
context.go
@ -55,14 +55,6 @@ const ContextRequestKey ContextKeyType = 0
|
|||||||
// abortIndex represents a typical value used in abort functions.
|
// abortIndex represents a typical value used in abort functions.
|
||||||
const abortIndex int8 = math.MaxInt8 >> 1
|
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,
|
// 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.
|
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
@ -997,7 +989,8 @@ func (c *Context) ClientIP() string {
|
|||||||
|
|
||||||
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||||
for _, headerName := range c.engine.RemoteIPHeaders {
|
for _, headerName := range c.engine.RemoteIPHeaders {
|
||||||
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
|
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
|
||||||
|
ip, valid := c.engine.validateHeader(headerValue)
|
||||||
if valid {
|
if valid {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1143,6 +1143,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
|
||||||
|
// Multiple X-Forwarded-For headers
|
||||||
|
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||||
|
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
|
||||||
|
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||||
|
|
||||||
|
c.engine.ForwardedByClientIP = true
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||||
|
|
||||||
|
// Should return 5.6.7.8 (last non-trusted IP)
|
||||||
|
assert.Equal(t, "5.6.7.8", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextClientIPWithSingleHeader(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
|
||||||
|
c.Request.RemoteAddr = localhostIP + ":1234"
|
||||||
|
|
||||||
|
c.engine.ForwardedByClientIP = true
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
_ = c.engine.SetTrustedProxies([]string{localhostIP})
|
||||||
|
|
||||||
|
// Should return 1.2.3.4
|
||||||
|
assert.Equal(t, "1.2.3.4", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that the response is serialized as Secure JSON
|
// Tests that the response is serialized as Secure JSON
|
||||||
// and Content-Type is set to application/json
|
// and Content-Type is set to application/json
|
||||||
func TestContextRenderSecureJSON(t *testing.T) {
|
func TestContextRenderSecureJSON(t *testing.T) {
|
||||||
@ -1910,7 +1941,7 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
resetContextForClientIPTests(c)
|
resetContextForClientIPTests(c)
|
||||||
|
|
||||||
// IPv6 support
|
// IPv6 support
|
||||||
c.Request.RemoteAddr = "[::1]:12345"
|
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
|
||||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
resetContextForClientIPTests(c)
|
resetContextForClientIPTests(c)
|
||||||
@ -3212,7 +3243,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
addr := strings.Split(l.Addr().String(), ":")
|
addr := strings.Split(l.Addr().String(), ":")
|
||||||
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
|
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(fmt.Errorf("request error: %w", err))
|
t.Error(fmt.Errorf("request error: %w", err))
|
||||||
return
|
return
|
||||||
|
|||||||
6
debug.go
6
debug.go
@ -13,7 +13,9 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ginSupportMinGoVer = 23
|
const ginSupportMinGoVer = 24
|
||||||
|
|
||||||
|
var runtimeVersion = runtime.Version()
|
||||||
|
|
||||||
// IsDebugging returns true if the framework is running in debug mode.
|
// IsDebugging returns true if the framework is running in debug mode.
|
||||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||||
@ -77,7 +79,7 @@ func getMinVer(v string) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func debugPrintWARNINGDefault() {
|
func debugPrintWARNINGDefault() {
|
||||||
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
|
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
|
||||||
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
|
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@ -21,10 +20,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO
|
|
||||||
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
|
||||||
// func debugPrint(format string, values ...any) {
|
|
||||||
|
|
||||||
func TestIsDebugging(t *testing.T) {
|
func TestIsDebugging(t *testing.T) {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
assert.True(t, IsDebugging())
|
assert.True(t, IsDebugging())
|
||||||
@ -48,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
|
|||||||
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
|
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugPrintFunc(t *testing.T) {
|
||||||
|
DebugPrintFunc = func(format string, values ...any) {
|
||||||
|
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
|
||||||
|
}
|
||||||
|
re := captureOutput(t, func() {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
debugPrint("debug print func test: %d", 123)
|
||||||
|
SetMode(TestMode)
|
||||||
|
})
|
||||||
|
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDebugPrintError(t *testing.T) {
|
func TestDebugPrintError(t *testing.T) {
|
||||||
re := captureOutput(t, func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||||||
debugPrintWARNINGDefault()
|
debugPrintWARNINGDefault()
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
})
|
})
|
||||||
m, e := getMinVer(runtime.Version())
|
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||||
if e == nil && m < ginSupportMinGoVer {
|
}
|
||||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
|
||||||
} else {
|
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
|
||||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
runtimeVersion = "go1.23.12"
|
||||||
}
|
re := captureOutput(t, func() {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
debugPrintWARNINGDefault()
|
||||||
|
SetMode(TestMode)
|
||||||
|
})
|
||||||
|
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||||
|
|||||||
37
gin.go
37
gin.go
@ -11,7 +11,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -48,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
|
||||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||||
type HandlerFunc func(*Context)
|
type HandlerFunc func(*Context)
|
||||||
|
|
||||||
@ -141,10 +135,16 @@ type Engine struct {
|
|||||||
AppEngine bool
|
AppEngine bool
|
||||||
|
|
||||||
// UseRawPath if enabled, the url.RawPath will be used to find parameters.
|
// 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
|
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.
|
// 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.
|
// as url.Path gonna be used, which is already unescaped.
|
||||||
UnescapePathValues bool
|
UnescapePathValues bool
|
||||||
|
|
||||||
@ -197,6 +197,7 @@ var _ IRouter = (*Engine)(nil)
|
|||||||
// - HandleMethodNotAllowed: false
|
// - HandleMethodNotAllowed: false
|
||||||
// - ForwardedByClientIP: true
|
// - ForwardedByClientIP: true
|
||||||
// - UseRawPath: false
|
// - UseRawPath: false
|
||||||
|
// - UseEscapedPath: false
|
||||||
// - UnescapePathValues: true
|
// - UnescapePathValues: true
|
||||||
func New(opts ...OptionFunc) *Engine {
|
func New(opts ...OptionFunc) *Engine {
|
||||||
debugPrintWARNINGNew()
|
debugPrintWARNINGNew()
|
||||||
@ -214,6 +215,7 @@ func New(opts ...OptionFunc) *Engine {
|
|||||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||||
TrustedPlatform: defaultPlatform,
|
TrustedPlatform: defaultPlatform,
|
||||||
UseRawPath: false,
|
UseRawPath: false,
|
||||||
|
UseEscapedPath: false,
|
||||||
RemoveExtraSlash: false,
|
RemoveExtraSlash: false,
|
||||||
UnescapePathValues: true,
|
UnescapePathValues: true,
|
||||||
MaxMultipartMemory: defaultMultipartMemory,
|
MaxMultipartMemory: defaultMultipartMemory,
|
||||||
@ -689,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
httpMethod := c.Request.Method
|
httpMethod := c.Request.Method
|
||||||
rPath := c.Request.URL.Path
|
rPath := c.Request.URL.Path
|
||||||
unescape := false
|
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
|
rPath = c.Request.URL.RawPath
|
||||||
unescape = engine.UnescapePathValues
|
unescape = engine.UnescapePathValues
|
||||||
}
|
}
|
||||||
@ -776,8 +782,8 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
req := c.Request
|
req := c.Request
|
||||||
p := req.URL.Path
|
p := req.URL.Path
|
||||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
prefix = sanitizePathChars(prefix)
|
||||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
prefix = removeRepeatedChar(prefix, '/')
|
||||||
|
|
||||||
p = prefix + "/" + req.URL.Path
|
p = prefix + "/" + req.URL.Path
|
||||||
}
|
}
|
||||||
@ -788,6 +794,17 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
redirectRequest(c)
|
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 {
|
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
rPath := req.URL.Path
|
rPath := req.URL.Path
|
||||||
|
|||||||
246
ginS/gins_test.go
Normal file
246
ginS/gins_test.go
Normal 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)
|
||||||
|
}
|
||||||
@ -70,9 +70,10 @@ func TestRunEmpty(t *testing.T) {
|
|||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run())
|
assert.NoError(t, router.Run())
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:8080/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":8080"))
|
require.Error(t, router.Run(":8080"))
|
||||||
testRequest(t, "http://localhost:8080/example")
|
testRequest(t, "http://localhost:8080/example")
|
||||||
@ -213,9 +214,10 @@ func TestRunEmptyWithEnv(t *testing.T) {
|
|||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run())
|
assert.NoError(t, router.Run())
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:3123/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":3123"))
|
require.Error(t, router.Run(":3123"))
|
||||||
testRequest(t, "http://localhost:3123/example")
|
testRequest(t, "http://localhost:3123/example")
|
||||||
@ -234,9 +236,10 @@ func TestRunWithPort(t *testing.T) {
|
|||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
assert.NoError(t, router.Run(":5150"))
|
assert.NoError(t, router.Run(":5150"))
|
||||||
}()
|
}()
|
||||||
// have to wait for the goroutine to start and run the server
|
|
||||||
// otherwise the main thread will complete
|
// Wait for server to be ready with exponential backoff
|
||||||
time.Sleep(5 * time.Millisecond)
|
err := waitForServerReady("http://localhost:5150/example", 10)
|
||||||
|
require.NoError(t, err, "server should start successfully")
|
||||||
|
|
||||||
require.Error(t, router.Run(":5150"))
|
require.Error(t, router.Run(":5150"))
|
||||||
testRequest(t, "http://localhost:5150/example")
|
testRequest(t, "http://localhost:5150/example")
|
||||||
|
|||||||
74
gin_test.go
74
gin_test.go
@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestH2c(t *testing.T) {
|
func TestH2c(t *testing.T) {
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
ln, err := net.Listen("tcp", localhostIP+":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@ -545,6 +545,29 @@ func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRebuild404Handlers(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) {
|
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||||
@ -720,6 +743,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) {
|
|||||||
assert.Equal(t, int64(1), handlerCounterV2)
|
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) {
|
func TestPrepareTrustedCIRDsWith(t *testing.T) {
|
||||||
r := New()
|
r := New()
|
||||||
|
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -7,12 +7,12 @@ require (
|
|||||||
github.com/gin-contrib/sse v1.1.0
|
github.com/gin-contrib/sse v1.1.0
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/goccy/go-json v0.10.2
|
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/json-iterator/go v1.1.12
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/modern-go/reflect2 v1.0.2
|
github.com/modern-go/reflect2 v1.0.2
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/quic-go/quic-go v0.56.0
|
github.com/quic-go/quic-go v0.57.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/ugorji/go/codec v1.3.1
|
github.com/ugorji/go/codec v1.3.1
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.47.0
|
||||||
@ -32,7 +32,7 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // 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
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
|
|||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -49,10 +49,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@ -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 letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
const (
|
const (
|
||||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
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
|
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||||
|
|
||||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||||
|
|||||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||||
// for p, eliminating . and .. elements.
|
// for p, eliminating . and .. elements.
|
||||||
//
|
//
|
||||||
@ -19,7 +21,6 @@ package gin
|
|||||||
//
|
//
|
||||||
// If the result of this process is an empty string, "/" is returned.
|
// If the result of this process is an empty string, "/" is returned.
|
||||||
func cleanPath(p string) string {
|
func cleanPath(p string) string {
|
||||||
const stackBufSize = 128
|
|
||||||
// Turn empty string into "/"
|
// Turn empty string into "/"
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return "/"
|
return "/"
|
||||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
|||||||
}
|
}
|
||||||
b[w] = c
|
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])
|
||||||
|
}
|
||||||
|
|||||||
47
path_test.go
47
path_test.go
@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveRepeatedChar(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
str string
|
||||||
|
char byte
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
str: "",
|
||||||
|
char: 'a',
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noSlash",
|
||||||
|
str: "abc",
|
||||||
|
char: ',',
|
||||||
|
want: "abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "withSlash",
|
||||||
|
str: "/a/b/c/",
|
||||||
|
char: '/',
|
||||||
|
want: "/a/b/c/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "withRepeatedSlashes",
|
||||||
|
str: "/a//b///c////",
|
||||||
|
char: '/',
|
||||||
|
want: "/a/b/c/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "threeSlashes",
|
||||||
|
str: "///",
|
||||||
|
char: '/',
|
||||||
|
want: "/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
res := removeRepeatedChar(tc.str, tc.char)
|
||||||
|
assert.Equal(t, tc.want, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
57
recovery.go
57
recovery.go
@ -5,7 +5,9 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -21,9 +23,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dunno = "???"
|
const (
|
||||||
|
dunno = "???"
|
||||||
var dunnoBytes = []byte(dunno)
|
stackSkip = 3
|
||||||
|
)
|
||||||
|
|
||||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||||
type RecoveryFunc func(c *Context, err any)
|
type RecoveryFunc func(c *Context, err any)
|
||||||
@ -68,8 +71,10 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
|
||||||
|
brokenPipe = true
|
||||||
|
}
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
const stackSkip = 3
|
|
||||||
if brokenPipe {
|
if brokenPipe {
|
||||||
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
|
||||||
} else if IsDebugging() {
|
} else if IsDebugging() {
|
||||||
@ -117,8 +122,11 @@ func stack(skip int) []byte {
|
|||||||
buf := new(bytes.Buffer) // the returned data
|
buf := new(bytes.Buffer) // the returned data
|
||||||
// As we loop, we open files and read them. These variables record the currently
|
// As we loop, we open files and read them. These variables record the currently
|
||||||
// loaded file.
|
// loaded file.
|
||||||
var lines [][]byte
|
var (
|
||||||
var lastFile string
|
nLine string
|
||||||
|
lastFile string
|
||||||
|
err error
|
||||||
|
)
|
||||||
for i := skip; ; i++ { // Skip the expected number of frames
|
for i := skip; ; i++ { // Skip the expected number of frames
|
||||||
pc, file, line, ok := runtime.Caller(i)
|
pc, file, line, ok := runtime.Caller(i)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -127,25 +135,44 @@ func stack(skip int) []byte {
|
|||||||
// Print this much at least. If we can't find the source, it won't show.
|
// Print this much at least. If we can't find the source, it won't show.
|
||||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||||
if file != lastFile {
|
if file != lastFile {
|
||||||
data, err := os.ReadFile(file)
|
nLine, err = readNthLine(file, line-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
|
||||||
lastFile = file
|
lastFile = file
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), cmp.Or(nLine, dunno))
|
||||||
}
|
}
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// source returns a space-trimmed slice of the n'th line.
|
// readNthLine reads the nth line from the file.
|
||||||
func source(lines [][]byte, n int) []byte {
|
// It returns the trimmed content of the line if found,
|
||||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
// or an empty string if the line doesn't exist.
|
||||||
if n < 0 || n >= len(lines) {
|
// If there's an error opening the file, it returns the error.
|
||||||
return dunnoBytes
|
func readNthLine(file string, n int) (string, error) {
|
||||||
|
if n < 0 {
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
return bytes.TrimSpace(lines[n])
|
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner.Scan() {
|
||||||
|
return strings.TrimSpace(scanner.Text()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// function returns, if possible, the name of the function containing the PC.
|
// function returns, if possible, the name of the function containing the PC.
|
||||||
|
|||||||
@ -88,21 +88,6 @@ func TestPanicWithAbort(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSource(t *testing.T) {
|
|
||||||
bs := source(nil, 0)
|
|
||||||
assert.Equal(t, dunnoBytes, bs)
|
|
||||||
|
|
||||||
in := [][]byte{
|
|
||||||
[]byte("Hello world."),
|
|
||||||
[]byte("Hi, gin.."),
|
|
||||||
}
|
|
||||||
bs = source(in, 10)
|
|
||||||
assert.Equal(t, dunnoBytes, bs)
|
|
||||||
|
|
||||||
bs = source(in, 1)
|
|
||||||
assert.Equal(t, []byte("Hello world."), bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFunction(t *testing.T) {
|
func TestFunction(t *testing.T) {
|
||||||
bs := function(1)
|
bs := function(1)
|
||||||
assert.Equal(t, dunno, bs)
|
assert.Equal(t, dunno, bs)
|
||||||
@ -142,6 +127,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) {
|
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||||
errBuffer := new(strings.Builder)
|
errBuffer := new(strings.Builder)
|
||||||
buffer := new(strings.Builder)
|
buffer := new(strings.Builder)
|
||||||
@ -307,3 +316,53 @@ func TestSecureRequestDump(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReadNthLine tests the readNthLine function with various scenarios.
|
||||||
|
func TestReadNthLine(t *testing.T) {
|
||||||
|
// Create a temporary test file
|
||||||
|
testContent := "line 0 \n line 1 \nline 2 \nline 3 \nline 4"
|
||||||
|
tempFile, err := os.CreateTemp("", "testfile*.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
// Write test content to the temporary file
|
||||||
|
if _, err := tempFile.WriteString(testContent); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tempFile.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lineNum int
|
||||||
|
fileName string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Read first line", lineNum: 0, fileName: tempFile.Name(), want: "line 0", wantErr: false},
|
||||||
|
{name: "Read middle line", lineNum: 2, fileName: tempFile.Name(), want: "line 2", wantErr: false},
|
||||||
|
{name: "Read last line", lineNum: 4, fileName: tempFile.Name(), want: "line 4", wantErr: false},
|
||||||
|
{name: "Line number exceeds file length", lineNum: 10, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||||
|
{name: "Negative line number", lineNum: -1, fileName: tempFile.Name(), want: "", wantErr: false},
|
||||||
|
{name: "Non-existent file", lineNum: 1, fileName: "/non/existent/file.txt", want: "", wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := readNthLine(tt.fileName, tt.lineNum)
|
||||||
|
assert.Equal(t, tt.wantErr, err != nil)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStack(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for b.Loop() {
|
||||||
|
_ = stack(stackSkip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -128,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
|||||||
// Flush implements the http.Flusher interface.
|
// Flush implements the http.Flusher interface.
|
||||||
func (w *responseWriter) Flush() {
|
func (w *responseWriter) Flush() {
|
||||||
w.WriteHeaderNow()
|
w.WriteHeaderNow()
|
||||||
w.ResponseWriter.(http.Flusher).Flush()
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// CreateTestContext returns a fresh Engine and a Context associated with it.
|
// 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
|
// 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)
|
c.writermem.reset(w)
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
|||||||
9
tree.go
9
tree.go
@ -5,7 +5,6 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@ -78,14 +77,6 @@ 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 {
|
func countParams(path string) uint16 {
|
||||||
colons := strings.Count(path, ":")
|
colons := strings.Count(path, ":")
|
||||||
stars := strings.Count(path, "*")
|
stars := strings.Count(path, "*")
|
||||||
|
|||||||
23
utils.go
23
utils.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -18,6 +19,12 @@ import (
|
|||||||
// BindKey indicates a default bind key.
|
// BindKey indicates a default bind key.
|
||||||
const BindKey = "_gin-gonic/gin/bindkey"
|
const BindKey = "_gin-gonic/gin/bindkey"
|
||||||
|
|
||||||
|
// localhostIP indicates the default localhost IP address.
|
||||||
|
const localhostIP = "127.0.0.1"
|
||||||
|
|
||||||
|
// localhostIPv6 indicates the default localhost IPv6 address.
|
||||||
|
const localhostIPv6 = "::1"
|
||||||
|
|
||||||
// Bind is a helper function for given interface object and returns a Gin middleware.
|
// Bind is a helper function for given interface object and returns a Gin middleware.
|
||||||
func Bind(val any) HandlerFunc {
|
func Bind(val any) HandlerFunc {
|
||||||
value := reflect.ValueOf(val)
|
value := reflect.ValueOf(val)
|
||||||
@ -162,3 +169,19 @@ func isASCII(s string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -148,3 +149,13 @@ func TestIsASCII(t *testing.T) {
|
|||||||
assert.True(t, isASCII("test"))
|
assert.True(t, isASCII("test"))
|
||||||
assert.False(t, isASCII("🧡💛💚💙💜"))
|
assert.False(t, isASCII("🧡💛💚💙💜"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSafeInt8(t *testing.T) {
|
||||||
|
assert.Equal(t, int8(100), safeInt8(100))
|
||||||
|
assert.Equal(t, int8(math.MaxInt8), safeInt8(int(math.MaxInt8)+123))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeUint16(t *testing.T) {
|
||||||
|
assert.Equal(t, uint16(100), safeUint16(100))
|
||||||
|
assert.Equal(t, uint16(math.MaxUint16), safeUint16(int(math.MaxUint16)+123))
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user